CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/multi-tool-finding-triage

Use when several security scanners (SAST, DAST, secrets, SCA, container, IaC) produce separate reports and the team needs one BLOCK or PASS. Normalizes and dedupes findings, records which scanners agree, rejects waivers without expiry or approver, adds EPSS and KEV, and writes one PR comment.

Quality

90%

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

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