{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://git.fromaitochitta.com/open/llm-security-commons/raw/branch/main/schema/finding.schema.json", "version": "0.1.0", "status": "normative", "title": "Finding", "description": "The finding contract: what a detector emits, and how that shape maps onto the SARIF and JSONL output profiles. Normative for the field names, types and severity vocabulary. A consumer that emits a differently-named field is not producing a finding under this contract, however similar the meaning.", "$comment": "Seeded from llm-security/scanners/lib/sarif-formatter.mjs (operator dump `dump-01-sarif-formatter.mjs`, 2026-08-09). READ THE EVIDENCE NOTE in `provenance.evidence_limits` before tightening anything here: that file CONSUMES findings, it does not produce them. It therefore proves the shape of the fields SARIF reads, and says nothing at all about fields it ignores. `additionalProperties` is deliberately left open for exactly that reason.", "provenance": { "source_repo": "llm-security", "source_files": ["scanners/lib/sarif-formatter.mjs"], "source_delivery": "operator dump, 2026-08-09", "source_commit": "unknown — not supplied with the dump", "verified": "partially", "evidence_limits": [ "The formatter is a consumer. Any finding field it does not read is invisible in this evidence, so the property list here is a LOWER BOUND on what a finding carries, never the full set.", "`title` is the only property the source PROVES to be required: `f.title.replace(...)` throws if it is absent. `scanner` and `severity` are required here by design, not by evidence — the formatter tolerates their absence (producing the rule id `undefined/...`, and defaulting the level to `note` respectively). See `open_questions`.", "The finding PRODUCER (the scan orchestrator that builds the envelope) has not been supplied. Until it is, this schema cannot be called closed." ] }, "type": "object", "$ref": "#/$defs/finding", "$defs": { "severity": { "description": "The finding severity vocabulary. Ordered most to least severe; both seed implementations use these five values.", "type": "string", "enum": ["critical", "high", "medium", "low", "info"] }, "owasp": { "description": "An OWASP taxonomy anchor, e.g. `LLM05`. Resolved against mapping/owasp-map.json. Optional: a finding may predate its taxonomy entry.", "type": "string", "minLength": 1 }, "finding": { "type": "object", "required": ["scanner", "title", "severity"], "additionalProperties": true, "$comment": "`additionalProperties: true` is a statement about the evidence, not laxness. Closing it would assert that no finding carries a field the SARIF formatter ignores, which this seed source cannot support.", "properties": { "scanner": { "description": "Identifier of the detector that produced the finding. Forms the first segment of the SARIF rule id.", "type": "string", "minLength": 1 }, "title": { "description": "Short human-readable name of the detected class. Forms the second segment of the SARIF rule id, and is the fallback for both `description` and the SARIF result message.", "type": "string", "minLength": 1 }, "severity": { "$ref": "#/$defs/severity" }, "description": { "description": "Longer explanation. Falls back to `title` wherever it is absent.", "type": "string" }, "recommendation": { "description": "What to do about it. Carried through to SARIF as a result property, not as part of the message.", "type": "string" }, "owasp": { "$ref": "#/$defs/owasp" }, "file": { "description": "Path the finding is anchored to. A finding without `file` has no SARIF location at all.", "type": "string" }, "line": { "description": "1-indexed line within `file`. Silently dropped if `file` is absent — see `known_lossiness`.", "type": "integer", "minimum": 1 } } }, "scannerResult": { "description": "One scanner's contribution to an envelope.", "type": "object", "additionalProperties": true, "properties": { "findings": { "type": "array", "items": { "$ref": "#/$defs/finding" } } } }, "envelope": { "description": "The multi-scanner container the SARIF profile is generated from. A missing or empty `scanners` map is legal and yields an empty SARIF run rather than an error.", "type": "object", "additionalProperties": true, "properties": { "scanners": { "type": "object", "additionalProperties": { "$ref": "#/$defs/scannerResult" } } } } }, "profiles": { "$comment": "Profiles are mapping rules, not JSON Schema keywords — a validator ignores this key, so the file stays a usable schema while carrying the mappings the plan requires alongside it.", "sarif": { "status": "normative", "spec": "SARIF 2.1.0 (OASIS)", "spec_url": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/", "schema_url": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", "sarif_version": "2.1.0", "tool": { "name": "llm-security", "information_uri": "https://git.fromaitochitta.com/open/llm-security", "version_default": "6.0.0", "$comment": "`name` and `information_uri` identify the SEED implementation. A different consumer emitting SARIF under this profile MUST substitute its own tool identity — copying these would attribute its findings to llm-security." }, "external_dependencies": "none", "rule_id": { "template": "{scanner}/{slug(title)}", "slug": { "steps": [ "replace every run of whitespace with a single `-`", "lowercase" ], "pattern_applied": "\\s+", "replacement": "-" } }, "severity_to_level": { "critical": "error", "high": "error", "medium": "warning", "low": "note", "info": "note" }, "severity_to_level_default": "note", "severity_to_level_note": "The mapping is many-to-one and NOT invertible: `critical` and `high` both become `error`, `low` and `info` both become `note`. A consumer that round-trips a finding through SARIF loses the distinction. Findings, not SARIF, are the interchange format here.", "rules_array": { "keyed_by": "rule_id", "first_wins": true, "first_wins_note": "The rule is built from the FIRST finding carrying a given rule id, so `fullDescription` reflects that finding's `description`. Each individual result keeps its own message, so nothing is lost at result level — but two findings sharing a title and differing in description produce one rule text.", "fields": { "id": "rule_id", "name": "title", "shortDescription.text": "title", "fullDescription.text": "description, falling back to title", "defaultConfiguration.level": "severity_to_level[severity]", "properties.tags": "[owasp] when present, otherwise []" } }, "result": { "fields": { "ruleId": "rule_id", "ruleIndex": "index into runs[0].tool.driver.rules", "level": "severity_to_level[severity]", "message.text": "description, falling back to title", "properties.tags": "[owasp] — omitted entirely when owasp is absent", "properties.recommendation": "recommendation — omitted when absent", "locations": "single-element array, present only when `file` is present" }, "location_shape": { "physicalLocation.artifactLocation.uri": "file", "physicalLocation.region.startLine": "line — added only when `line` is present" } }, "envelope_traversal": "Findings are collected across every value of `envelope.scanners`, in object-iteration order, and flattened into one run. Scanner identity survives only inside each finding's own `scanner` field.", "runs": "exactly one" }, "jsonl": { "status": "unspecified", "$comment": "TODO — NOT extracted, and deliberately not invented. The plan calls for a JSONL profile, but no JSONL emitter was supplied with the dump, and the obvious reading (one finding object per line) is inference, not evidence. Guessing here would publish a normative contract nobody implements. Needed to close it: whichever module in llm-security writes JSONL output.", "needed_source": "llm-security — the JSONL/NDJSON output writer", "must_not_assume": [ "that a line holds a bare finding rather than a wrapper carrying scanner or run context", "that severity is emitted as the finding vocabulary rather than the SARIF level", "that field order, or a trailing newline on the final line, is unconstrained" ] } }, "known_lossiness": [ { "id": "rule-id-collision", "claim": "Two titles that differ only in letter case, or only in which whitespace runs separate their words, produce ONE rule id — silently merging two classes into one entry in the SARIF rules array. The merged rule carries the FIRST finding's description; each result keeps its own message, so nothing is lost at result level.", "does_not_apply_to": "Punctuation. The slug lowercases and collapses whitespace and nothing else, so `Zero-width carrier` and `Zero-width carrier!` stay distinct ids.", "verified": "`A B` + `a b` -> 1 rule / 2 results, rule fullDescription `first`, result messages [`first`,`second`]. `Zero-width carrier` + `Zero-width carrier!` -> ids [`sc/zero-width-carrier`, `sc/zero-width-carrier!`]." }, { "id": "line-without-file-dropped", "claim": "A finding carrying `line` but no `file` produces no SARIF location at all; the line number is discarded without warning.", "verified": "`{line: 9}` with no `file` -> `result.locations` is absent." }, { "id": "severity-not-invertible", "claim": "critical and high both map to `error`; low and info both map to `note`. A finding round-tripped through SARIF cannot recover its original severity.", "verified": "[critical, high, medium, low, info] -> [error, error, warning, note, note]." } ], "open_questions": [ "Required-field set. Only `title` is enforced by the seed implementation; `scanner` and `severity` are required here by design. Adopting this schema as a validator in llm-security could therefore reject findings it currently emits. That is a behaviour change and belongs in llm-security's own phase, decided against its own tests — not here.", "Cross-runtime field names. The Python guard's Finding carries `label`, `detector`, `count`, `evidence` and `source`; only `severity` and `owasp` are common with the Node shape. A single contract two runtimes conform to needs either a rename in one of them or a declared alias table. Deferred to v0.2.0 with the decision stated, rather than settled silently here.", "The finding producer (scan orchestrator) has not been supplied, so the property list is a lower bound." ] }