CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/multi-tool-finding-triage

Merges two or more security scanner reports into one gate. Use when you need a single BLOCK or PASS decision from multiple scanners instead of reading N separate reports. Normalizes each report into one common finding format (a canonical `Finding`), deduplicates on a per-domain key while recording which scanners agree (`caught_by` consensus), validates a waiver (finding-suppression) file, rejecting any missing `expires:` / `approved_by:` / `reason:` or expired, enriches CVE findings with EPSS (exploit-probability) and CISA KEV (known-exploited catalog), then applies a `fail_on` severity threshold to emit BLOCK or PASS plus a bucketed pull-request comment. Works across static (SAST), dynamic (DAST), secret, dependency (SCA), container, and IaC scanners. To run a single scanner instead use semgrep-rules, codeql-queries, or one of the language-native-sast linters; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.

77

Quality

97%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

waiver-schema.mdreferences/

Waiver file schema and validation

Waivers live in one committed YAML file per domain, named for the gate it feeds (.sast-waivers.yaml, .dast-waivers.yaml, .secrets-waivers.yaml, .sca-waivers.yaml, .vuln-waivers.yaml, .iac-waivers.yaml). Step 5 of multi-tool-finding-triage references this file.

Schema

One schema, with exact-match keys or *_pattern glob keys:

waivers:
  # exact-match waiver, code / policy domain
  - scanner: scanner-a
    rule_id: js/hardcoded-credentials
    file: src/dev-only-server.js
    line: 42
    reason: "Dev-only server; runs on localhost without TLS by design"
    expires: 2026-12-31
    approved_by: alice@example.com

  # pattern waiver: any scanner, any matching path
  - scanner_pattern: "*"
    rule_id_pattern: "K8S_*"
    file_pattern: "helm/dev-overrides/**"
    reason: "Dev overrides; not deployed to production"
    expires: 2026-09-30
    approved_by: platform-team

  # CVE-domain waiver
  - cve: CVE-2024-1234
    package: lodash@4.17.20
    reason: "Vulnerable function not in the call path; verified via dependency tree"
    expires: 2026-12-31
    approved_by: alice@example.com

Matching keys per domain: scanner / rule_id / file / line for code and policy findings, url_pattern / finding_class for dynamic findings, cve / package for CVE findings. Any *_pattern variant takes a glob.

Validation rules

A waiver that fails any of these is rejected, and the underlying finding stays active:

  • expires: missing.
  • expires: in the past relative to today.
  • approved_by: missing or empty.
  • reason: missing or empty.

A rejected waiver is never a silent no-op. Report it explicitly, with the reason for rejection, so the author fixes the waiver instead of assuming it applied.

REQUIRED = ('expires', 'approved_by', 'reason')

def validate_waiver(w, today):
    for field in REQUIRED:
        if not w.get(field):
            return f"missing `{field}:`"       # rejection reason, or None if valid
    return f"expired {w['expires']}" if w['expires'] < today else None

Refuse-to-proceed rules

  • Never waive a CVE listed in CISA KEV. Active exploitation in the wild admits no acceptable justification (https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json).
  • Never accept a VEX not_affected status with an empty justification (https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md).
  • Never auto-fix a finding. Report and recommend only.
  • Never suppress a whole severity bucket. Low and info findings still appear in the report, below the fold.

SKILL.md

tile.json