Catalyst / admin/Synapse-Cortex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Cortex

public

Self Hosted ITSM Tool with RBAC/Tenanting and MFA

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Synapse-Cortex / Synapse-Cortexv2 / docs / vuln-remediation-hitl / verify_diff.py 3970 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
"""
Phase 5 verification for the Human-in-the-Loop Vulnerability Remediation playbook.

The playbook's Phase 1 and Phase 5 nodes run the EXACT SAME predefined discovery
command (see playbooks/vuln-remediation-hitl-linux.json — node data.command). Its
output is one package name per line: the set of packages with a pending upgrade.

A CVE is confirmed remediated when the package that carried it drops OUT of that
set between the baseline scan (Phase 1) and the verification scan (Phase 5).

This module is intentionally dependency-free and deterministic so the assistant
can call it verbatim and record the structured result on the ticket.
"""

from __future__ import annotations

from dataclasses import dataclass, field, asdict


def parse_scan(raw: str) -> set[str]:
    """Turn raw discovery output into a set of still-upgradable package names.

    The command already emits `sort -u`'d one-per-line names, so parsing is just
    a normalise-and-collect. Kept tolerant of blank lines / stray whitespace so a
    noisy SSH capture can't silently skew the diff."""
    return {line.strip() for line in (raw or "").splitlines() if line.strip()}


@dataclass
class VerificationResult:
    resolved: list[str] = field(default_factory=list)      # target pkgs now patched
    still_vulnerable: list[str] = field(default_factory=list)  # target pkgs still pending
    newly_upgradable: list[str] = field(default_factory=list)  # regressions introduced
    success: bool = False
    summary: str = ""


def verify_remediation(baseline_raw: str, verify_raw: str,
                       target_packages: list[str]) -> VerificationResult:
    """Diff the two identical-command scans and decide, per target package,
    whether the vulnerability was resolved.

    Args:
        baseline_raw:   raw stdout of the Phase 1 scan.
        verify_raw:     raw stdout of the Phase 5 scan (same command).
        target_packages: the packages the approved remediation was meant to fix
                         (derived from the ticket's vulnerabilities[].package).

    Success == every targeted package left the "upgradable" set AND the scan did
    not introduce a new upgradable package (no unexpected regression)."""
    before = parse_scan(baseline_raw)
    after = parse_scan(verify_raw)
    targets = {p.strip() for p in target_packages if p and p.strip()}

    resolved = sorted(p for p in targets if p in before and p not in after)
    still_vulnerable = sorted(p for p in targets if p in after)
    # A package that appears in the verify scan but was NOT pending before is a
    # regression the patch pulled in — surfaced, but not counted as our success.
    newly_upgradable = sorted((after - before) - targets)

    success = bool(targets) and not still_vulnerable

    if success:
        summary = (f"Verified: {', '.join(resolved) or 'no change needed'} no longer "
                   f"reports a pending security upgrade.")
    else:
        summary = ("Verification FAILED — still pending: "
                   f"{', '.join(still_vulnerable) or '(no targets supplied)'}.")
    if newly_upgradable:
        summary += f" Note: new upgradable packages appeared: {', '.join(newly_upgradable)}."

    return VerificationResult(
        resolved=resolved,
        still_vulnerable=still_vulnerable,
        newly_upgradable=newly_upgradable,
        success=success,
        summary=summary,
    )


if __name__ == "__main__":
    # Illustrative self-check: samba was patched, libssl was not.
    baseline = "libssl3\nsamba\nsudo\n"
    verify = "libssl3\nsudo\n"
    res = verify_remediation(baseline, verify, target_packages=["samba"])
    assert res.success and res.resolved == ["samba"], res
    res2 = verify_remediation(baseline, verify, target_packages=["samba", "libssl3"])
    assert not res2.success and res2.still_vulnerable == ["libssl3"], res2
    import json
    print(json.dumps(asdict(res), indent=2))
    print(json.dumps(asdict(res2), indent=2))