Skip to content

Service signatures

Writing and distributing YAML signatures

Edit this page
On this page

A signature is how Loomscope turns "port 3000 is open" into "this is Grafana". There are two sources — compiled and YAML — producing one internal type, so a custom signature is not a second-class citizen.

Why two sources

Core signatures are compiled into the daemon binary. The set is kept small on purpose: every one of them runs against every discovered port, so they are a performance and a safety surface.

Custom signatures are YAML files loaded at boot and reloaded on SIGHUP. They exist because no vendor will ever fingerprint your in-house applications, and rebuilding a scanner binary to identify one is not a reasonable thing to ask.

Writing one

Drop a file into /etc/loomscope/signatures.d/ — or wherever LOOMSCOPE_SIGNATURES_DIR points — and send SIGHUP.

yaml
version: 1
service:
  id: internal-auth-service
  name: Internal Auth Service
  description: In-house OIDC provider
  category: Web
  vendor: ACME Corp
  icon: https://internal.corp/icons/auth.svg
  confidence_default: 85
  pattern:
    all_of:
      - port: { protocol: tcp, number: 8443 }
      - endpoint:
          port: { protocol: tcp, number: 8443 }
          tls: true
          path: /healthz
          expect: "auth-service v"
FieldRequiredNotes
versionYesAlways 1 today
service.idYesStable identifier. Changing it creates a different service
service.nameYesWhat the UI shows
service.descriptionNoOne line
service.categoryYesSee the category list below
service.vendorNo
service.iconNoAn icon set reference or a URL
service.confidence_defaultYes0–100. What a match is worth
service.patternYesThe matching rule

Pattern variants

The pattern enum is identical in YAML and in Go, so a custom signature can express exactly what a compiled one can:

VariantYAML
Port onlyport: { protocol: tcp, number: 8443 }
HTTP endpointendpoint: { port, path, expect, tls?, headers? }
Logical ANDall_of: [ … ]
Logical ORany_of: [ … ]
Negationnot: …
Is a gatewayis_gateway: true
MAC vendormac_vendor: "Cisco"
Subnet typesubnet_type: dmz
Manual onlynone: true

none: true defines a service that discovery will never match — useful for a service you assign by hand but want to appear consistently everywhere.

Default credentials

A signature can record what its product ships with:

yaml
service:
  id: acme-badge-reader
  name: ACME Badge Reader
  default_credentials:
    - username: admin
      password: acme1234
      note: The BR-200 ships with this and its web interface does not force a change.
      reference: https://example.invalid/br200

Nothing tries these. The claim is that the product ships with a documented login — a fact from the vendor's own page, true whether or not the deployment in front of you changed it. It is not a claim that this host accepts it, and Loomscope never attempts one: an authentication attempt against your estate lands in your logs, can lock an account, and on an appliance can lock the appliance. That is a different decision, with its own consent, and this is not it.

So a service page reads "ships with admin/admin — check whether this one was changed", never "accepts admin/admin".

note is required. Without it the interface can only say "default credentials", which tells an operator to worry and not what to do.

An entry with no username and no password means the product ships with no authentication at all — Redis, for instance. That is a different finding from a known password, and it reads differently.

Three compiled-in signatures declare one today: Grafana (admin/admin), Zabbix (Admin/zabbix, which unlike Grafana is not forced to change at first sign-in), and Redis (open by default). The YAML field exists so an estate full of appliances nobody upstream has written a signature for can be covered without rebuilding the daemon.

mac_vendor

The daemon resolves the manufacturer from the first bytes of a host's MAC address, using the IEEE assignment registry compiled into the binary. It covers all three registries — MA-L, MA-M and MA-S — and the longest prefix wins, which matters more than it sounds: a quarter of all assignments are sub-allocations inside blocks IEEE holds itself, so a lookup that stopped at the first three bytes would confidently report "IEEE Registration Authority" instead of the manufacturer.

The comparison is a case-insensitive substring. The registry spells Cisco Cisco Systems, Inc, so mac_vendor: "Cisco" is what you write. The consequence is worth knowing before you rely on it: mac_vendor: "HP" also matches Shenzhen HPTECH. Prefer the longest fragment that is unambiguous.

It never matches when the vendor is unknown, and there are three ways for that to happen:

  • The MAC is locally administered — assigned by a hypervisor, a container runtime or a phone randomising its address. There is no manufacturer to report.
  • The daemon could not read a MAC at all. Check the daemon's capabilities on the daemons screen; a platform with no neighbour cache reader attributes no MAC addresses.
  • The prefix is unassigned, or was assigned after the table in your daemon build was generated. The table is refreshed each release and never fetched at runtime, so an air-gapped install behaves identically.

Examples

A service identified by an endpoint response:

yaml
pattern:
  endpoint:
    port: { protocol: tcp, number: 9090 }
    path: /-/healthy
    expect: "Prometheus"

Either of two ports:

yaml
pattern:
  any_of:
    - port: { protocol: tcp, number: 6379 }
    - port: { protocol: tcp, number: 6380 }

A port, but not on a gateway:

yaml
pattern:
  all_of:
    - port: { protocol: tcp, number: 53 }
    - not: { is_gateway: true }

Hardware identified by its MAC vendor:

yaml
pattern:
  all_of:
    - mac_vendor: "Axis Communications"
    - port: { protocol: tcp, number: 80 }

Categories

Web, Database, Monitoring, Networking, Storage, Security, Messaging, Virtualization, Directory, Mail, Other.

Categories drive grouping and colour in the topology views, so picking a sensible one is worth thirty seconds.

Validation is strict, and fails at boot

An unknown field or an invalid enum value fails the file with a structured error in the logs. It is not silently ignored.

That is deliberate: a typo'd field in a permissive loader produces a signature that never matches, and nothing anywhere says why. A loud failure at boot is a worse first attempt and a much better tenth.

bash
docker logs loomscope-daemon | grep signature

The heartbeat reports how many core and custom signatures loaded, and the daemon list shows both counts — so you can confirm a new file took effect without reading logs at all.

Reloading

bash
docker kill --signal=HUP loomscope-daemon
# or
sudo systemctl reload loomscope-daemon

A reload does not interrupt a running scan. A file that fails validation leaves the previously loaded set in place rather than dropping it.

Confidence

confidence_default is what a match is worth on a scale of 0–100. Be honest with it:

EvidenceReasonable range
Unique endpoint response, distinctive string85–95
Endpoint response, generic string60–80
Banner match50–70
Port number alone20–40

Confidence propagates into vulnerability matching: an over-confident signature produces over-confident findings, and somebody will act on them.

Distributing signatures

Signatures are files, so distribute them however you distribute configuration — a config-management role, a mounted ConfigMap, a volume baked at image build. In Kubernetes, a ConfigMap mounted at /etc/loomscope/signatures.d and a rolling restart is the usual shape.

Testing

The pattern engine has a table-driven test suite with golden YAML inputs and expected match/no-match outputs, and the same suite runs against both the Go and the YAML paths — which is what keeps the two implementations of the enum from drifting apart.