Triage vulnerabilities and gate CI
Resolve and classify findings, then fail a CI build when a quality gate is breached.
The problem
A pentest produces findings, but findings only matter if they are triaged and if regressions are caught automatically. This recipe covers the full triage flow — resolve, false positive, accept risk, and bulk updates — and then wires a quality gate into CI so a build fails when, say, any critical vulnerability is still open.
Prerequisites
- A completed pentest with vulnerabilities (see Run your first automated pentest).
- An API token (see Authentication).
pip install rank-sdk
export RANK_API_KEY=rk_...
export RANK_PENTEST_ID=123
Steps
-
List the findings. Start from the current inventory, optionally filtered by severity. From the CLI this is
rank pentest vulns <pentestId>.vulns = client.pentests.vulnerabilities.list(pentest_id, severity="critical") for v in vulns.items: print(v.id, v.severity, v.status, v.vulnerability) -
Triage each finding. Use the dedicated transition methods.
resolveacceptswithout_evidence(default) orevidenced;false_positiveandaccept_riskrequire a reason.client.pentests.vulnerabilities.resolve( pentest_id, vuln_id, resolution_type="without_evidence", comment="Patched in the latest deployment.", ) client.pentests.vulnerabilities.false_positive( pentest_id, vuln_id, reason="Only reachable from the staging network, not production.", ) client.pentests.vulnerabilities.accept_risk( pentest_id, vuln_id, reason="Low-impact informational finding, accepted by management.", ) -
Bulk-update status. Move many findings to
openorin_progressin one call (use the dedicated methods above for resolved / false-positive / accepted-risk states).open_ids = [v.id for v in vulns.items if v.status == "open"] result = client.pentests.vulnerabilities.bulk_update_status( pentest_id, vulnerability_ids=open_ids, status="in_progress", comment="Batch triage - assigned to the security team.", ) print(result.updated, result.skipped) -
Evaluate a quality gate. Define rules and let Rank score them. The per-pentest gate returns
passedplus a list offailures.gate = client.pentests.vulnerabilities.quality_gate( pentest_id, rules=[ {"severity": "critical", "max_open": 0}, {"severity": "high", "max_open": 5}, {"min_resolution_rate": 0.8}, ], ) if not gate.passed: for f in gate.failures: print(f.rule, f.expected, f.actual)To check a rolling gate across your recent pentests instead, call the global gate:
summary = client.pentests.quality_gate(max_open_critical=0, min_resolution_rate=80) print(summary.overall_passed, summary.pass_count, summary.fail_count) -
Fail the build in CI. Commit the gate script to your own repository as
quality_gate.pyand run it as a step. Because the script exits non-zero when the gate fails, the job fails too.name: Security gate on: [push, pull_request] jobs: rank-quality-gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install rank-sdk - name: Enforce Rank quality gate env: RANK_API_KEY: ${{ secrets.RANK_API_KEY }} RANK_PENTEST_ID: ${{ vars.RANK_PENTEST_ID }} run: python quality_gate.py
Run it
Save the following as quality_gate.py. It exits with status 1 when the gate fails, 0 when
it passes, and 2 on configuration or API errors. Set RANK_API_KEY and RANK_PENTEST_ID,
then run python quality_gate.py. Commit the same file to your own repository so the CI step
above can run it.
"""Triage vulnerabilities and enforce a quality gate in CI.
This script is designed to run inside a CI pipeline. It:
1. Lists the vulnerabilities of a completed pentest.
2. (Optionally) applies a triage pass that demonstrates the status
transitions: resolve, false positive, accept risk, and a bulk update.
The mutating calls only run when RANK_APPLY_TRIAGE=1 so the script is
safe to run repeatedly in CI.
3. Evaluates a quality gate against the pentest's vulnerabilities.
4. Exits with a non-zero status code when the gate fails, which fails the
CI build.
Run:
pip install rank-sdk
export RANK_API_KEY=rk_...
export RANK_PENTEST_ID=123
python quality_gate.py
Optional environment variables:
RANK_APPLY_TRIAGE Set to "1" to run the (mutating) triage demonstration.
"""
from __future__ import annotations
import os
import sys
import rank
PENTEST_ID = int(os.environ.get("RANK_PENTEST_ID", "0"))
APPLY_TRIAGE = os.environ.get("RANK_APPLY_TRIAGE") == "1"
# A finding that breaks any of these rules fails the gate (and the build).
GATE_RULES = [
{"severity": "critical", "max_open": 0},
{"severity": "high", "max_open": 5},
{"min_resolution_rate": 0.8},
]
def show_inventory(client: rank.Rank) -> None:
"""Print the current vulnerabilities grouped by what they are."""
vulns = client.pentests.vulnerabilities.list(PENTEST_ID)
print(f"Pentest #{PENTEST_ID}: {len(vulns.items)} vulnerabilities on this page")
for v in vulns.items:
print(f" [{v.id}] {v.severity:>8} {v.status:>12} {v.vulnerability}")
def apply_triage(client: rank.Rank) -> None:
"""Demonstrate the status transitions used during triage.
Only runs when RANK_APPLY_TRIAGE=1. Every call is guarded so an
unexpected current status does not abort the whole run.
"""
vulns = client.pentests.vulnerabilities.list(PENTEST_ID)
if not vulns.items:
print("Nothing to triage.")
return
first = vulns.items[0]
try:
client.pentests.vulnerabilities.resolve(
PENTEST_ID, first.id,
resolution_type="without_evidence",
comment="Patched in the latest deployment.",
)
print(f"Resolved #{first.id}")
except rank.APIError as exc:
print(f"Could not resolve #{first.id}: {exc.message}")
if len(vulns.items) >= 2:
second = vulns.items[1]
try:
client.pentests.vulnerabilities.false_positive(
PENTEST_ID, second.id,
reason="Only reachable from the staging network, not production.",
)
print(f"Marked #{second.id} as false positive")
except rank.APIError as exc:
print(f"Could not flag #{second.id}: {exc.message}")
if len(vulns.items) >= 3:
third = vulns.items[2]
try:
client.pentests.vulnerabilities.accept_risk(
PENTEST_ID, third.id,
reason="Low-impact informational finding, accepted by management.",
)
print(f"Accepted risk on #{third.id}")
except rank.APIError as exc:
print(f"Could not accept risk on #{third.id}: {exc.message}")
open_ids = [v.id for v in vulns.items if v.status == "open"][:50]
if open_ids:
result = client.pentests.vulnerabilities.bulk_update_status(
PENTEST_ID,
vulnerability_ids=open_ids,
status="in_progress",
comment="Batch triage - assigned to the security team.",
)
print(f"Bulk update: {result.updated} updated, {result.skipped} skipped")
def evaluate_gate(client: rank.Rank) -> bool:
"""Evaluate the quality gate and print any failures. Returns passed."""
gate = client.pentests.vulnerabilities.quality_gate(PENTEST_ID, rules=GATE_RULES)
print("\n--- Quality gate ---")
if gate.summary is not None:
s = gate.summary
print(f" Total open vulns : {s.by_status.get('open', 0)}")
print(f" Resolution rate : {s.resolution_rate}")
if gate.passed:
print(" Result: PASSED")
return True
print(" Result: FAILED")
for failure in gate.failures:
print(f" - {failure.rule}: expected {failure.expected}, got {failure.actual}")
return False
def main() -> int:
if PENTEST_ID <= 0:
print("ERROR: set RANK_PENTEST_ID to a completed pentest ID.")
return 2
with rank.Rank() as client:
show_inventory(client)
if APPLY_TRIAGE:
print("\n--- Applying triage (RANK_APPLY_TRIAGE=1) ---")
apply_triage(client)
else:
print("\nSkipping mutating triage (set RANK_APPLY_TRIAGE=1 to enable).")
passed = evaluate_gate(client)
# A failing gate must fail the CI build.
return 0 if passed else 1
if __name__ == "__main__":
try:
sys.exit(main())
except rank.AuthenticationError:
print("ERROR: invalid or missing API key. Set RANK_API_KEY.")
sys.exit(2)
except rank.NotFoundError:
print(f"ERROR: pentest #{PENTEST_ID} not found.")
sys.exit(2)
except rank.APIError as exc:
print(f"API error ({exc.status_code}): {exc.message}")
sys.exit(2)
For more on the underlying status model, see Vulnerabilities.