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, bandit-python, or gosec-go; this runs after them to merge output - the cross-scanner gate, not a single-scanner wrapper.

74

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

cve-enrichment.mdreferences/

CVE enrichment and the verdict threshold

Enrichment applies only where a finding carries a CVE. Severity alone ranks static danger; two public feeds add real-world exploitation signal. Steps 4 and 6 of multi-tool-finding-triage reference this file.

EPSS and CISA KEV

EPSS is "a daily estimate of the probability of exploitation activity being observed over the next 30 days", scored 0 to 1 with a percentile (https://www.first.org/epss/model).

CISA KEV is the "CISA Catalog of Known Exploited Vulnerabilities": vulnerabilities with reliable evidence of exploitation in the wild. The JSON feed exposes catalogVersion, count, and a vulnerabilities[] array whose entries carry cveID, dateAdded, and requiredAction (https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json).

# EPSS bulk daily feed; columns are cve,epss,percentile after a #model_version header
curl -sL https://epss.empiricalsecurity.com/epss_scores-current.csv.gz | gunzip > epss.csv
grep "^CVE-2021-44228," epss.csv

# EPSS per-CVE API
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2021-44228"
# {"data":[{"cve":"CVE-2021-44228","epss":"0.999990000","percentile":"1.000000000", ...}]}

# CISA KEV membership
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev.json
jq '.vulnerabilities[] | select(.cveID == "CVE-2021-44228")' kev.json

VEX and reachability

VEX, where the project publishes one, filters findings already analyzed by the vendor or the team. OpenVEX defines four statuses: not_affected, affected, fixed, under_investigation, and a not_affected statement "required the addition of a justification" (https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md). Filter on not_affected only when that justification is populated, and list filtered findings in an audit table rather than dropping them.

Reachability is optional and, without runtime instrumentation, always heuristic: an unused dependency, a dev-only or test-scope dependency, a vulnerable API that is never imported. Treat a false reachability result as a strong signal to deprioritize, never as proof of safety.

Priority buckets

Priority buckets for CVE domains, replacing the raw severity sort:

def priority(f):
    if f.get('vex_status') == 'not_affected':
        return 'Filtered-VEX'          # surfaced for audit; does not block
    if f.get('in_kev'):
        return 'Fix-Now'               # exploited in the wild
    if f['severity'] == 'critical' and (f.get('epss') or 0) > 0.5:
        return 'Fix-Now'
    if f['severity'] == 'critical':
        return 'Fix-This-Sprint'
    if f['severity'] == 'high' and (f.get('epss') or 0) > 0.3:
        return 'Fix-This-Sprint'
    if f.get('reachable') is False:
        return 'Fix-Backlog'
    if f['severity'] == 'high':
        return 'Fix-This-Sprint'
    if f['severity'] == 'medium':
        return 'Fix-Backlog'
    return 'Accept-Risk'

The 0.5 and 0.3 EPSS cut-offs are starting points, not standards: EPSS publishes probabilities and expects each organization to pick thresholds matching its own capacity and risk tolerance (https://www.first.org/epss/model). Record the chosen values in the repository next to the triage script.

The verdict threshold

The gate is one comparison against a configured fail_on level, using the same severity rank as the merge:

def verdict(findings, fail_on='critical'):
    rank = {'critical': 5, 'high': 4, 'medium': 3, 'low': 2, 'info': 1}
    threshold = rank.get(fail_on, 5)
    blocking = [f for f in findings if rank.get(f['severity'], 0) >= threshold]
    return ('BLOCK', blocking) if blocking else ('PASS', [])

Default fail_on is critical: any unwaived critical finding blocks. Infrastructure policy gates commonly run at fail_on: high, where misconfiguration findings cluster. Two domains swap the severity test for a bucket test: secret detection blocks on any surviving Verified finding (a live credential has no low-severity reading), and CVE domains block on the Fix-Now bucket above.

The verdict runs on the post-waiver list, and a finding whose waiver was rejected is still in that list.

SKILL.md

tile.json