diff --git a/nyx-scan/action.yml b/nyx-scan/action.yml index 84626bb..207e91d 100644 --- a/nyx-scan/action.yml +++ b/nyx-scan/action.yml @@ -46,6 +46,13 @@ runs: run: | .nyx-src/target/release/nyx scan --format json --quiet > nyx-results-raw.json 2>nyx-scan.stderr + # Per-finding triage decisions (from the nyx serve UI) are keyed by a blake3 + # portable fingerprint. Best-effort install so the gate can honor them; if it + # can't be installed the gate falls back to (rule_id, path) matching. + python3 -c 'import blake3' 2>/dev/null \ + || python3 -m pip install --quiet --break-system-packages blake3 2>/dev/null \ + || true + python3 -c " import json, os @@ -61,8 +68,10 @@ runs: with open('.nyx/triage.json') as f: triage = json.load(f) rules = triage.get('suppression_rules', []) + decisions = triage.get('decisions', []) except: rules = [] + decisions = [] def rel_path(p): p = p.replace(workspace, '').lstrip('/') @@ -76,7 +85,41 @@ runs: i = rid.find(' (source ') return rid[:i] if i != -1 else rid + # Per-finding triage decisions (nyx serve UI), keyed by portable fingerprint: + # blake3(id \0 rel_path \0 sink_snippet \0 source_snippet \0 func) — mirrors + # nyx server/models.rs compute_portable_fingerprint. States open/investigating + # still block; suppressed/false_positive/accepted_risk do not. If blake3 is + # unavailable, fall back to (rule_id, path) — coarser but never crashes the gate. + SUPPRESSING = ('suppressed', 'false_positive', 'accepted_risk') + active = [d for d in decisions if d.get('state') in SUPPRESSING] + try: + import blake3 + def portable_fp(f): + ev = f.get('evidence') or {} + sink = ((ev.get('sink') or {}).get('snippet')) or '' + src = ((ev.get('source') or {}).get('snippet')) or '' + func = '' + for s in (ev.get('flow_steps') or []): + if s.get('function'): + func = s['function'] + break + data = '\0'.join([f.get('id', ''), rel_path(f.get('path', '')), sink, src, func]).encode('utf-8') + return blake3.blake3(data).hexdigest() + decided_fps = set(d.get('fingerprint', '') for d in active) + def decided(f): + return portable_fp(f) in decided_fps + if active: + print('decisions: matching', len(active), 'by portable fingerprint (blake3)') + except ImportError: + decided_keys = set((d.get('rule_id', ''), d.get('path', '')) for d in active) + def decided(f): + return (f.get('id', ''), rel_path(f.get('path', ''))) in decided_keys + if active: + print('decisions: blake3 unavailable, matching', len(active), 'by (rule_id, path)') + def is_suppressed(f): + if decided(f): + return True rule_id = base_rule(f.get('id', '')) path = rel_path(f.get('path', '')) for r in rules: