SIS CLI

Asset Graph & Asset-Path Queries

Every scanned file is modelled as a nested asset graph: the root document plus every artefact it contains — archive entries, email parts, OOXML/OLE members, PDF embedded files and carved blobs, and polyglot interpretations — as a single recursive tree. The engine always emits one (a bare root for a flat document), so report.asset_graph is the source of truth for structure-aware querying.

You address nodes in that tree with an asset-path selector, an abbreviated-XPath grammar over the containment structure.

The asset-path grammar

An asset path is /-separated segments of <tag>:<name>, where <tag> is the container grammar tag (zip, pdf, ole2, mht, iso, eml) and <name> is the member label. Wildcards and predicates scope the match:

  • pdf:root — the root node.
  • zip:root/zip:doc.pdf — a named entry.
  • * — any single segment; ** — any depth (recursive descent).
  • [field=value] — a predicate on a node’s typed fields (see below).
  • A trailing /<terminal> projects something about the matched nodes (findings, digest, text, …).
sis query "**" bundle.zip                          # every asset node
sis query "zip:root/*" bundle.zip                  # direct members of the root
sis query "**/[format=pe]" bundle.zip              # any PE anywhere in the tree
sis query "**[polyglot=true]" doc.pdf            # nodes in a polyglot

Output envelope: { "result": { "type": "asset_select", "source": "asset_graph", "matched_count", "nodes": [...] } }. A selector runs live (sis query "<selector>" <file>) or over a saved report without rescanning (sis query --from-report report.json "<selector>").

Predicates

[field=value] filters matched nodes on typed asset fields. Supported fields:

  • format — the sniffed format (pe, pdf, zip, png, js, …). This is what routing keys on.
  • shape — the structural identity (container.zip, markup.html, script.js, …): the source-qualified structural authority’s answer to “what is this artefact?”. Distinct from format (the coarse sniff) — [shape=…] matches only where a shape probe structurally resolved that format. One answer per node, even on a polyglot.
  • candidate_shape — membership over every accepted shape candidate. Where [shape=…] asks what the artefact is, [candidate_shape=…] asks whether a format was accepted at all, so a polyglot’s non-selected views stay queryable: a PDF carrying a script-shaped member matches both [shape=document.pdf] and [candidate_shape=script.vbs].
  • extent — the honest validation depth of any shape candidate (magic, header, header_and_footer, index, physical_layout, whole_structure, …). [extent=header] = shallow-validated members.
  • origin — root / email_part / archive_entry / embedded_object / carved.
  • depth — nesting depth (numeric: >=, =, >, <, <=).
  • complete — true/false (a truncated/over-budget member is false).
  • analysis_state, declared_type, sniffed_type.
  • finding — membership: [finding=<kind>] matches nodes carrying that finding kind.
  • sha256 / blake3 — the decoded-content digest (the content address is blake3).
  • format, observed_format, format_family — a member the scanner has not identified as a known format (plain text, unknown, an unrecognised sniff) is unknown for all three: a format literal is canonicalised (format=ole is format=ole2, format=sevenzip is format=7z) and compared with the canonical tag the graph records. The graph’s format is unknown for plain text and for an unidentified member alike; format_state (below) is what tells them apart. So [format=pe] and [format!=pe] both exclude and count such a member, A format, observed_format or format_family literal must be an identified value: the canonical tag or an alias of it (case is ignored; format=PE is format=pe), or one of the registry’s family names. Anything else (exe, pe32, gzip, a typo, unknown, text) is a parse error, exit 2, because such a rule could never equal a recorded value (a deny would pass the executable). [format=unknown] cannot select the unidentified population and no predicate for it exists yet. sniffed_type is the sniffer’s raw string and takes any text. The family is derived from the canonical tag as well, so a sniff the graph cannot record as a format (such as gzip) has an unknown format_family in the graph.
  • format_state — why a node has, or lacks, an identified format: identified, text (the sniffer found text and no marker in the first 512 bytes: a member with an ASCII head and a binary tail reads text), empty (zero bytes, content address present), directory, unrecognised_binary (a complete read that nothing identified), not_sniffed (the bytes were never available), truncated (a prefix identified nothing). A recorded state is known for = and !=. A node from a report written before the field existed has no state and is unknown for both operators; [format_state=not_recorded] is a parse error. format_state changes nothing about format, observed_format or format_family: they stay unknown for every unidentified member. To let plain text through a deny-style rule, compose the exemption explicitly: **/[format_family=executable][format_state!=text] is No for a text member and still unknown for an unrecognised, truncated, empty or unsampled one. Only compose it for a format the sniffer identifies by magic bytes, and accept that text is a judgement of the head (pe, elf, pdf, zip, …): a markerless batch or PowerShell file sniffs as text and is still a script, so never exempt text from a rule on a script or markup format.
  • polyglot — true iff the node is a polyglot container or sits inside one. Only the positive is known: no node records that alternate-view detection ran (it does not on a scan without --deep) or that no view was dropped by the interpretation budget or a failed child scan, so [polyglot=false] is unknown for every node.
  • polyglot_views — interpretation count on a polyglot node (numeric). It is a lower bound: >=/> and the cases a bound settles (= or < below the recorded count) are known, the rest are unknown. An ordinary node has no known count.
  • view — scope to one polyglot interpretation, e.g. [view=zip].

A predicate on a field the node never recorded is unknown for both = and !=: [declared_type!=pdf] selects nodes whose declared type is recorded and is not pdf, never nodes that declared nothing. Fields that can be absent (sniffed_type literals are case-insensitive and reduce an identified format to its canonical tag, so PE is pe and ole is ole2; it has no fixed vocabulary, so other literals are not validated): role, declared_type, sniffed_type, verdict, blake3, sha256 (never computed, so always unknown), view and shape (outside a polyglot / without a shape assessment), the metadata fields producer, creator, toolchain, identity, binding and coverage (none recorded), candidate_shape and extent (no shape assessment). An empty metadata category is unknown, not a known empty set: a node carries metadata whenever any one category was recovered, so a node with only an identity says nothing about producers, bindings or coverage. The only exceptions are the identity and binding categories, whose extraction lanes record a completed marker. The rule is the same as for findings: a value that was found stands (= true, != false), while “not found” (= false, != true) is known only when the lane completed; otherwise it is unknown, even when the category has other recorded entries. producer, creator, toolchain, coverage, candidate_shape and extent have no completion marker, so their “not found” is always unknown.

[finding=x] and [withheld=x] are true when the finding was found, whatever the node’s state. Their absence is only known for a node whose own analysis is complete; for a node that was not attempted, partial, failed or refused, [finding!=x] and a not-found [finding=x] are unknown (a truncated subtree, complete=false, does not change this: it concerns the node’s descendants, not its own findings).

Predicate values are checked when the selector is parsed: depth and polyglot_views take integers, complete and polyglot take true/false, and ordered operators apply to numeric fields only. sis query leaves unknown nodes out and reports excluded_unknown; sis sanitize plan treats them as matched; sis extract --asset refuses. A known-false sibling predicate still rules a node out, so [format=pe][verdict=x] does not block a PDF member.

sis query "**/[format=pe]" sample.eml                       # executables at any depth
sis query "**/[format=ooxml][complete=false]" sample.zip    # truncated OOXML members
sis query "**/[format=pe][polyglot=true]" doc.pdf         # a PE inside any polyglot
sis query "**/[format=pe][view=zip]" doc.pdf              # scoped to the zip interpretation
sis query "**/[shape=container.ooxml]" bundle.zip           # members whose structural identity IS OOXML
sis query "**/[candidate_shape=container.zip]" bundle.zip   # members where ZIP was accepted as any candidate
sis query "**/[extent=header]" bundle.zip                   # only header-deep (shallow) structural members

Terminals

A trailing /<terminal> projects something about the matched nodes:

  • /findings — the findings attributed to each node.
  • /summary — a compact per-node summary (path, format, counts).
  • /sha256 — the decoded-content digest (reported as blake3 + digest_algo).
  • /views — for a polyglot node, its interpretations (view_format, verdict, finding count).
  • /text — each matched asset’s extracted safe-text (live scan only — a saved report carries handles, not bytes, so --from-report … /text refuses).
  • /bytes — write each matched asset’s decoded bytes to disk. Egress-gated: refuses without --extract-to <dir> (raw member bytes never go to stdout) and in report mode. Returns a manifest (asset_path, output_path, size, blake3) — never inline bytes.
sis query "**/[format=ooxml]/text" bundle.zip                       # per-member safe-text
sis query "**/[format=pe]/sha256" bundle.zip                        # per-member digest
sis query "**[polyglot=true]/views" doc.pdf                       # interpretations
sis query "**/[format=pe]/bytes" bundle.zip --extract-to /tmp/out   # carve out the PEs

Scope note: /text and /bytes resolve for the root and first-level container members (the flat member seam); deeply-nested members report unavailable honestly.

Polyglots

A polyglot is one asset whose bytes parse validly as multiple formats. It is modelled as a single node with one interpretation subtree per format. Selectors are transparent through the polyglot wrapper: zip/pdf reaches a PDF via zip → (polyglot) → pdf without naming the polyglot. Query for polyglot relationships with [polyglot=true], [polyglot_views>=2], [view=<fmt>], and /views.

  • sis docs query — the full sis query namespace + --where grammar
  • sis docs shape — clustering files by asset-graph shape (campaign hunting)
  • sis docs extract — extract --asset <selector> scoped extraction
  • sis docs cdr — sis sanitize plan --asset scoped-drop planning