Skip to content

Latest commit

 

History

History

snipe-it

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

Custom Integration: Snipe-IT

Imports hardware assets from Snipe-IT, the open-source IT asset management system. Snipe-IT is a purchasing-and-ownership record rather than a discovery tool, so what it contributes to runZero is the half of an asset that a network scan cannot see: asset tag, serial number, purchase date, warranty expiry, assigned user, and location.

runZero requirements

Snipe-IT requirements

  • API Bearer Token for Snipe-IT API authentication
  • Snipe-IT Base URL (depends on local install)
  • At least one field that runZero can merge on. See the note under runZero configuration.

Snipe-IT API Docs

Steps

Snipe-IT configuration

  1. Decide which account the token will belong to. Snipe-IT API tokens are personal access tokens: they are generated by a signed-in user and carry that user's permissions. A token minted from an admin account is an admin token. Create a dedicated user for this integration and give it a group whose permissions allow API access and viewing assets — nothing else. That way the token cannot modify your asset register even if it leaks.

  2. Sign in as that user and generate an API token from the account's own API keys page. The token is a long JWT and is displayed once; copy it immediately.

  3. Find your base URL for the Snipe-IT API

    • This will be the hostname or host:port of the Snipe-IT server
    • example: https://my.snipe.org
    • example: https://192.168.5.64:8443
    • Give the base URL only, with no trailing /. The script appends /api/v1/hardware itself. This used to be a SNIPE_BASE_URL global inside the script; it is now the url credential parameter and the global no longer exists.
  4. Confirm the token works from the Explorer host. Snipe-IT returns HTTP 200 with an HTML login page rather than a 401 when the token is not accepted, so check the body, not the status code:

    curl -s -H 'Accept: application/json' \
      -H 'Authorization: Bearer <token>' \
      'https://my.snipe.org/api/v1/hardware?limit=1'

    A working token returns JSON with total and a rows array.

How the register is paged. The script walks /api/v1/hardware with limit and offset, streaming each page as it is received, and keeps going until the number of rows it has read reaches the total the API reports. Snipe-IT's own default when no limit is sent is 50 rows — that default was once the whole import, so an install with more hardware than that silently imported a fraction of its register. There is no page-size ceiling on the import now.

Progress is in the task log, one line per page:

snipe-it: reported 500 assets from 500 of 1284 hardware records
snipe-it: reported 998 assets from 1000 of 1284 hardware records
snipe-it: reported 1281 assets from 1284 of 1284 hardware records

The first number is assets imported and the second is hardware rows read; they differ when a row is skipped for having no id, asset_tag or serial, which is logged on its own line. The third is Snipe-IT's total, so the last line of a complete run has its second and third numbers equal.

runZero configuration

  1. (OPTIONAL) - make any neccessary changes to the script to align with your environment
    • runZero requires at least one common field to merge assets with. The default Snipe-IT fieldset "Asset with MAC address" tracks MAC addresses and this is the minimum field needed for runZero asset merging to take effect (though this field may not always be available with runZero scanned assets)
    • The base script will map Snipe's "category" to runZero's asset "type"; maintaining category naming conventions consistent to runZero's type values would be beneficial
    • "Model" and "Manufacturer" fields in Snipe are also mapped to the corresponding "Model" and "Manufacturer" attributes in runZero
    • Add additional custom fields that are configured in Snipe-IT
      • Snipe-IT is highly configurable and many custom fields and fieldsets can be created for assets
      • Tracking additional fields such as os, os version, and hostname could be beneficial for merging. If adding these custom fields to fieldset then map them in the custom integration script accordingly
      • Any additional custom fields tracked in Snipe-IT that do not correspond to runZero's ImportAsset data model can be mapped to custom attributes following the pattern in the script
  2. Create the Credential for the Custom Integration
    • Select the type Custom Integration Script Secrets
    • Snipe-IT URL (url): base URL of the Snipe-IT server, no trailing slash
    • API token (api_token): the bearer token generated above
    • Hardware per page (page_size, optional): rows requested per request, default 500. Lower it if a large page times out on a slow install. An install that caps limit server-side below this value does not truncate the import; paging continues until total is reached either way.
    • Maximum assets (max_assets, optional): a safety valve for a very large register, default 0, which imports everything. Set a positive number to stop the run early; a run stopped that way says so in the task log and names the limit, so it is never silent.
  3. Create the Custom Integration
    • Add a Name and Icon (e.g. Snipe-IT)
      • The script embeds its CONFIG block, so the credential form is generated automatically from the fields above
      • The custom_integration: search value comes from the script's CONFIG id with the runzero- prefix removed — snipe-it here — not from the display name you type
    • Toggle Enable custom integration script to input your finalized script
    • Click Validate to ensure it has valide syntax
    • Click Save to create the Custom Integration
  4. Create the Custom Integration task
    • Select the Credential and Custom Integration created in steps 2 and 3
    • Update the task schedule to recur at the desired timeframes. An asset register changes slowly, so daily is usually plenty.
    • Select the Explorer you'd like the Custom Integration to run from
    • Click Save to kick off the first task

Running it from the command line

The runZero Explorer binary runs a script directly, which is the fastest way to see what an integration would import before scheduling it. --kwargs is repeated once per parameter:

runzero script --filename snipe-it/snipe-it.star \
  --kwargs url=https://my.snipe.org \
  --kwargs api_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.ExampleFakeSnipeItToken.0123456789abcdef \
  --custom-integration-id 1f2e3d4c-5b6a-7988-9a0b-1c2d3e4f5a6b \
  --output ./snipe-it-run

--output writes the assets the run produced. The scanner refuses to write into a directory that already exists, so add --overwrite when re-running into the same path. Add --verbose for the request-by-request log, or omit --output to see only the log lines. The per-page progress lines described above report the running count as the run proceeds; the last one should end with N of N hardware records, matching total from the curl check.

--kwargs takes the value verbatim as long as the whole argument holds a single =, so a comma inside a value is passed through intact. Only a value that also contains an = flips the flag into comma-separated parsing, and then the value is cut at the first comma — the remainder either becomes a fabricated second parameter or aborts the run with must be formatted as key=value. Neither character appears in a Snipe-IT token: it is a JWT, dot-separated base64url with no padding.

To check the CONFIG block and the HTTP and TLS wiring without a live server:

runzero script --filename snipe-it/snipe-it.star --validate

Validation generates placeholder parameter values and answers from a local dummy server, so it proves the script initializes, declares its parameters correctly, and issues a request. It does not prove Snipe-IT accepts the token or that any asset is parsed. The fixture scenarios are what exercise the parsing — the happy path, an empty install, rows with no MAC, a row that leaks a MAC into an unexpected field, and a register spread over two pages:

python3 tests/run.py snipe-it

The same script also runs under the scan command, which is what the platform itself invokes for a scheduled task:

runzero scan --custom-integration-script-source "$(cat snipe-it/snipe-it.star)" \
  --custom-integration-id <uuid-from-the-console> \
  --custom-integration-script-kwargs 'url=https://my.snipe.org,api_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.ExampleFakeSnipeItToken.0123456789abcdef' \
  --output ./snipe-it-scan

--custom-integration-id is the UUID the console shows on the integration's page. --custom-integration-entry-function-name defaults to main and only needs setting for a script with a different entry point. Note that --custom-integration-script-kwargs takes one comma-separated string; prefer script --kwargs for ad-hoc runs.

What's next?

  • You will see the task kick off on the tasks page like any other integration
  • The task will update the existing assets with the data pulled from the Custom Integration source
  • The task will create new assets for when there are no existing assets that meet merge criteria (hostname, MAC, etc)
  • You can search for assets enriched by this custom integration with the runZero search custom_integration:snipe-it. The CONFIG id is runzero-snipe-it, so the slug is snipe-it; if that returns nothing, check the name you gave the integration in the console.

Asset identity

  • Target entity: a hardware record in the Snipe-IT asset register, from GET /api/v1/hardware. This is an inventory record about a device, not an observation of one — Snipe-IT is an IT asset management system, so a row exists because somebody entered it, not because anything saw it on a network.

  • Source ID field: id, falling back to asset_tag, then to serial. A row with none of the three is skipped and logged.

  • Documentation evidence: id is the primary key of Snipe-IT's assets table and the value every hardware sub-route is addressed by — GET /api/v1/hardware/{id}, and the checkin/checkout actions on it. Snipe-IT's own API reference documents it as the identifier for a hardware record and distinguishes it explicitly from asset_tag, which is the human-facing label, and from GET /api/v1/hardware/bytag/{tag}, the separate lookup that exists precisely because the two are different things.

  • Uniqueness scope: one Snipe-IT install, and the id says so. This is the property this integration handles best. Snipe-IT's hardware id is a per-instance auto-increment integer — every install numbers its register from 1 — so a bare 1 would collide with the 1 of every other Snipe-IT. The id is therefore built as snipe-it:<url-hostname>:hardware:<id>, scoped on the hostname parsed from the configured url. Two installs imported into one runZero account stay apart, which is exactly the treatment ninjaone, snow-license-manager, and wazuh all need and do not have.

  • Cardinality: one row per hardware record. A device entered twice in Snipe-IT is two rows and two assets; that is a data-quality problem in the register rather than something this integration can resolve.

  • Stability: the id is stable for the life of the row. It does not survive deleting and re-creating the record, which is how a register is commonly corrected, so expect churn on an estate where asset records are rebuilt rather than edited.

    The scope is the fragile half. It is derived from the configured URL's hostname, so moving a Snipe-IT install to a new hostname, or re-pointing the credential from an IP to a DNS name, changes the scope and re-identifies the entire register in one run — every asset is minted fresh and every previous one is stranded. Only the hostname is used, so a scheme change or a port change is harmless; a hostname change is not.

  • Reuse behavior: auto-increment integers are not normally recycled by MySQL, but Snipe-IT publishes no statement and a restored-from-backup database can certainly re-issue them.

  • Presence: id is present on every hardware row. The two fallbacks are defensive, and they are worth understanding because they change the shape of the identity: asset_tag and serial are strings, so a row that falls back produces snipe-it:<host>:hardware:ACME-00123 rather than a number. That is still stable and still scoped, but a row that reports an id on one run and only a serial on the next changes identity. No such behavior has been observed.

  • Final runZero ID: snipe-it:<url-hostname>:hardware:<id>.

  • Missing-ID behavior: skip the row with snipe-it: skipping hardware with no id/asset_tag/serial. The run also refuses to start at all if the configured URL yields no hostname, rather than importing the whole register under an empty scope.

  • Match behavior: left at the platform default — all eight flags on.

  • Verdict: authoritative within one install, with the install boundary encoded in the id rather than left to the credential.

Why the default matchBehavior is kept, and what actually merges these assets

The id is a persistent, install-scoped remote identifier, so the governing rule points at foreign-ID matching and the code follows it.

The more interesting question for this source is not which flags are set but what there is to match on at all, because the answer is much less than for any other integration here:

  • Hostnames are asserted only when the name survives scrubbing. Snipe-IT's name field is a label an administrator typed, and on a real register it is routinely a person ("the owner's MacBook"), a location, or an inventory code rather than anything a name service would resolve. The name is therefore passed through the clean_hostname filter — which rejects free-text labels (anything with characters that cannot appear in a DNS name), placeholders, and IP-shaped values — and only a value that could actually be a hostname is imported as one. The raw name is always kept as the name custom attribute.
  • No OS, no OS version, no services, no software, no addresses in a stock install. Snipe-IT does not track any of them by default.
  • So correlation rests on the MAC address and the hostname-shaped names. A stock Snipe-IT has no MAC field at all; the value comes from a custom field, which is why the setup notes above single out the "Asset with MAC address" fieldset as the minimum configuration for reliable merging. Addresses come from custom fields too, parsed out with a comma split for multi-homed hosts.

The practical consequence: on an install with no MAC custom field and free-text asset names, this integration creates parallel assets rather than enriching existing ones, and no matchBehavior setting can change that — there is nothing to match on. Relaxing mac-break would make it worse, not better, by removing the guard on the strongest correlation signal available.

Future

  • Components, accessories, consumables, and licenses. GET /api/v1/components, /api/v1/accessories, /api/v1/consumables, and /api/v1/licenses are the other four halves of a Snipe-IT register. Licenses are the interesting one for runZero: a license record names a product and a seat count and can be checked out to a hardware record, so joining it to the assets this integration already imports would produce runZero Software entries with entitlement data attached — which is something no scanner can determine, because a scan sees what is installed and not what is licensed.
  • Maintenances as lifecycle context. GET /api/v1/maintenances records repairs, upgrades, and warranty work against a hardware id. The hardware record already contributes warranty.expiration and eol.date as attributes; the maintenance history is what turns those from dates into a support posture.
  • Users and checkout as ownership data. The hardware record carries user.checkout today as a bare attribute. GET /api/v1/users and the checkout endpoints would resolve it to a real person with a department and a location, which is the attribution layer runZero's own discovery cannot supply. Note that this is personal data and belongs behind a parameter rather than being imported unconditionally.
  • Server-side filtering instead of a full sweep. GET /api/v1/hardware accepts status, category_id, manufacturer_id, model_id, location_id, and a search term. Today the whole register is paged on every run and max_assets is a blunt cap that stops mid-way. A status or category filter would let an operator import only deployed hardware and skip archived, disposed, and pending rows — which on an old register is often most of it.
  • GET /api/v1/hardware/audit/due and /audit/overdue as a coverage report. Snipe-IT tracks when each asset was last audited and when the next audit is due. Cross-referenced with runZero's own last-seen data the pairing answers a question neither system can answer alone: which assets the register believes exist but nothing has seen on the network, and which assets are on the network but overdue for a register audit.
  • Outbound: runZero discovery as Snipe-IT records — the direction with real leverage. Snipe-IT's API is fully writable: POST /api/v1/hardware creates a hardware record, PATCH /api/v1/hardware/{id} updates one, and custom fields are writable alongside the built-ins. runZero routinely discovers devices that are not in the register at all, which is precisely the gap an ITAM system exists to close, and it discovers them with a MAC, an OS, and a fingerprint. An outbound integration could file unknown assets as pending Snipe-IT records for an administrator to confirm, or — more conservatively and probably more useful — write runZero's observed MAC and last-seen date onto existing records so the register stops relying on an administrator having typed them. Two warnings: creating hardware records from discovery will generate noise on a network with transient devices, so this wants a filter and a review step rather than a straight sync; and writing needs a token with far broader rights than the read-only one this integration asks for.
  • There is no event or alert feed. Snipe-IT has no webhook aimed at third-party consumers and no change cursor on the hardware endpoint — its notification features target email and Slack. Anything near-real-time would be re-paging /api/v1/hardware on a short schedule, and since a register changes slowly, that is a poor trade. The updated_at field the mapping already reads as last.seen would at least allow a client-side change detection if one were wanted.