Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/scripts/sigilix_sarif_contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import argparse
import json
import re
import sys


Expand Down Expand Up @@ -253,6 +254,66 @@ def _sanitize_run_results(run):
run["results"] = [result for result in results if isinstance(result, dict)]


# SIG-184 Phase 3 — canonical vuln-class stamping. The engine's receipt-join
# (verify-finding-receipt.ts) confirms a security finding into the VERIFIED tier
# only when an independent runner scan flagged the SAME canonical class at the
# same path/line. These class strings MUST match the engine's CanonicalVulnClass
# enum (src/shared/domain/vuln-class.ts) VERBATIM — keep the two in lockstep.
#
# CREDIBILITY (Codex + DeepSeek sign-off): NOT loose substring matching. We map a
# native tool ruleId to the set of classes whose anchored pattern matches; a
# result is stamped ONLY when EXACTLY ONE distinct class matches. Zero or >1
# matches → left UNSET (the engine treats absence as INCONCLUSIVE, never a false
# VERIFIED). So an ambiguous/colliding ruleId is fail-safe, never mis-stamped.
_RULE_CLASS_PATTERNS = {
"semgrep": [
(re.compile(r"dangerous-exec|command[-_.]?inj|os[-_.]?command|dangerous-subprocess|spawn.*shell"), "command-injection"),
(re.compile(r"nosql[-_.]?inj"), "nosql-injection"),
(re.compile(r"(?<![a-z])sql[-_.]?inj"), "sql-injection"),
(re.compile(r"path[-_.]?travers|directory[-_.]?travers|zip[-_.]?slip"), "path-traversal"),
(re.compile(r"(?<![a-z])ssrf|server[-_.]?side[-_.]?request"), "ssrf"),
(re.compile(r"(?<![a-z])xxe|xml[-_.]?external[-_.]?entit"), "xxe"),
(re.compile(r"(?<![a-z])ssti|template[-_.]?inj"), "ssti"),
(re.compile(r"proto(?:type)?[-_.]?pollution"), "proto-pollution"),
(re.compile(r"open[-_.]?redirect"), "open-redirect"),
(re.compile(r"hard[-_.]?coded|hardcoded"), "hardcoded-secret"),
(re.compile(r"insecure[-_.]?random|weak[-_.]?random|insecure.*prng"), "weak-random"),
],
}
# OpenGrep runs the same rulesets as Semgrep → reuse the patterns.
_RULE_CLASS_PATTERNS["opengrep"] = _RULE_CLASS_PATTERNS["semgrep"]


def stamp_rule_classes(run, tool_id):
"""Stamp per-result properties.sigilixRuleClass (collision-safe). Strips any
preexisting (untrusted) value first, then sets a class ONLY when exactly one
pattern-class matches the native ruleId. Mutates + returns ``run``."""
results = run.get("results")
if not isinstance(results, list):
return run
patterns = _RULE_CLASS_PATTERNS.get(tool_id)
for result in results:
if not isinstance(result, dict):
continue
props = result.get("properties")
# Always strip any preexisting class from raw tool output (untrusted).
if isinstance(props, dict):
props.pop("sigilixRuleClass", None)
if not patterns:
continue
rule_id = _result_rule_id(result)
if not rule_id:
continue
matched = {cls for (pattern, cls) in patterns if pattern.search(rule_id)}
if len(matched) != 1:
continue # 0 or >1 → fail-safe: leave unset (engine → INCONCLUSIVE).
if not isinstance(props, dict):
props = {}
result["properties"] = props
props["sigilixRuleClass"] = next(iter(matched))
return run


def _main(argv):
parser = argparse.ArgumentParser(description="Attach Sigilix metadata to SARIF runs.")
parser.add_argument("tool_id", choices=sorted(KNOWN_TOOL_IDS))
Expand All @@ -275,6 +336,9 @@ def _main(argv):
if args.cap is not None:
_, summary = cap_run_results(run, args.cap)
attach_sigilix_metadata(run, args.tool_id, summary)
# SIG-184 Phase 3: stamp the canonical vuln-class on each final result,
# after capping/metadata (so it runs over the assembled output).
stamp_rule_classes(run, args.tool_id)
normalized_runs.append(run)
if args.ensure_run and not normalized_runs:
normalized_runs.append(empty_metadata_run(args.tool_id))
Expand Down
40 changes: 40 additions & 0 deletions .github/scripts/sigilix_sarif_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_main as contract_main,
attach_sigilix_metadata,
cap_results,
stamp_rule_classes,
)
from sigilix_sarif_merge import _main as sarif_merge_main
from sigilix_sarif_merge import merge_sarif_documents
Expand Down Expand Up @@ -943,5 +944,44 @@ def test_normalize_path_falls_back_for_paths_outside_base_dir(self):
self.assertEqual(normalize_path("", base_dir="/repo"), ".")


class StampRuleClassesTest(unittest.TestCase):
"""SIG-184 Phase 3 — canonical vuln-class stamping (credibility core)."""

@staticmethod
def _run(rule_id, props=None):
result = {"ruleId": rule_id}
if props is not None:
result["properties"] = props
run = {"tool": {"driver": {"name": "Semgrep"}}, "results": [result]}
stamp_rule_classes(run, "semgrep")
return run["results"][0].get("properties", {}).get("sigilixRuleClass")

def test_stamps_command_injection(self):
self.assertEqual(
self._run("javascript.lang.security.audit.dangerous-exec.dangerous-exec"),
"command-injection",
)

def test_stamps_sql_not_nosql(self):
self.assertEqual(self._run("python.lang.security.audit.sql-injection"), "sql-injection")
self.assertEqual(self._run("javascript.lang.security.nosql-injection"), "nosql-injection")

def test_unknown_rule_left_unset(self):
self.assertIsNone(self._run("javascript.lang.best-practice.no-console-log"))

def test_strips_preexisting_untrusted_class(self):
# A class smuggled in raw tool output must be removed, then re-derived.
self.assertIsNone(self._run("some.unmapped.rule", {"sigilixRuleClass": "command-injection"}))

def test_collision_safe_unset_when_two_classes_match(self):
# A contrived ruleId hitting two patterns → exactly-one guard leaves it unset.
self.assertIsNone(self._run("sql-injection-and-ssrf-combo"))

def test_unknown_tool_id_strips_only(self):
run = {"tool": {"driver": {}}, "results": [{"ruleId": "dangerous-exec", "properties": {"sigilixRuleClass": "x"}}]}
stamp_rule_classes(run, "eslint") # no patterns for eslint → strip only
self.assertNotIn("sigilixRuleClass", run["results"][0]["properties"])


if __name__ == "__main__":
unittest.main()
Loading