vulnpilot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vulnpilot/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # VulnPilot by PatchVex
2
+ # Mission: Help security teams turn vulnerability scan data into prioritized action.
3
+ __version__ = "0.1.0"
vulnpilot/cli.py ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VulnPilot CLI by PatchVex
4
+ Usage:
5
+ vulnpilot analyze scan.csv
6
+ vulnpilot analyze scan.csv --all
7
+ vulnpilot update-feeds
8
+ vulnpilot --version
9
+ """
10
+ from __future__ import annotations
11
+ import argparse
12
+ import logging
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from vulnpilot import __version__
17
+ from vulnpilot.parser import parse_nessus_csv
18
+ from vulnpilot.enrich import enrich, update_feeds
19
+ from vulnpilot.scoring import score_all
20
+ from vulnpilot.reports import render_summary, render_findings, render_top_hosts, render_free_tier_gate
21
+
22
+ FREE_TIER_LIMIT = 20
23
+
24
+
25
+ def cmd_analyze(args: argparse.Namespace) -> int:
26
+ try:
27
+ findings = parse_nessus_csv(Path(args.csv))
28
+ except FileNotFoundError as e:
29
+ print(f"\n ERROR: {e}", file=sys.stderr)
30
+ return 1
31
+ except ValueError as e:
32
+ print(f"\n ERROR: {e}", file=sys.stderr)
33
+ return 1
34
+
35
+ if not findings:
36
+ print("\n No actionable findings in this CSV.")
37
+ return 0
38
+
39
+ enrich(findings,
40
+ kev_path=Path(args.kev) if args.kev else None,
41
+ epss_path=Path(args.epss) if args.epss else None)
42
+
43
+ scored = score_all(findings)
44
+ use_colour = sys.stdout.isatty() and not args.no_colour
45
+ is_paid = args.all or bool(args.license)
46
+ limit = len(scored) if is_paid else FREE_TIER_LIMIT
47
+
48
+ print(render_summary(scored, use_colour=use_colour))
49
+ print(render_findings(scored, limit=limit, use_colour=use_colour))
50
+ print(render_top_hosts(scored, top_n=args.top_hosts, use_colour=use_colour))
51
+
52
+ if not is_paid and len(scored) > FREE_TIER_LIMIT:
53
+ print(render_free_tier_gate(len(scored), FREE_TIER_LIMIT, use_colour=use_colour))
54
+
55
+ return 0
56
+
57
+
58
+ def cmd_update_feeds(args: argparse.Namespace) -> int:
59
+ try:
60
+ update_feeds(cache_dir=Path(args.cache) if args.cache else None)
61
+ except Exception as e:
62
+ print(f"\n ERROR: {e}", file=sys.stderr)
63
+ return 1
64
+ return 0
65
+
66
+
67
+ def build_parser() -> argparse.ArgumentParser:
68
+ parser = argparse.ArgumentParser(
69
+ prog="vulnpilot",
70
+ description="VulnPilot by PatchVex — Turn vulnerability scan data into prioritized action.",
71
+ )
72
+ parser.add_argument("--version", action="version", version=f"VulnPilot {__version__}")
73
+ parser.add_argument("--no-colour", "--no-color", action="store_true", dest="no_colour")
74
+
75
+ sub = parser.add_subparsers(dest="command")
76
+
77
+ analyze = sub.add_parser("analyze", help="Analyze a Nessus CSV export")
78
+ analyze.add_argument("csv", help="Path to Nessus CSV file")
79
+ analyze.add_argument("--kev", help="Path to local KEV JSON")
80
+ analyze.add_argument("--epss", help="Path to local EPSS CSV")
81
+ analyze.add_argument("--top-hosts", type=int, default=10, metavar="N")
82
+ analyze.add_argument("--all", action="store_true", help="Show all findings [Pro]")
83
+ analyze.add_argument("--license", metavar="KEY", help="License key [Pro]")
84
+
85
+ feeds = sub.add_parser("update-feeds", help="Download latest KEV and EPSS feeds")
86
+ feeds.add_argument("--cache", help="Cache directory")
87
+
88
+ return parser
89
+
90
+
91
+ def main() -> None:
92
+ logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s")
93
+ parser = build_parser()
94
+ args = parser.parse_args()
95
+ if not args.command:
96
+ parser.print_help()
97
+ sys.exit(0)
98
+ if args.command == "analyze":
99
+ sys.exit(cmd_analyze(args))
100
+ elif args.command == "update-feeds":
101
+ sys.exit(cmd_update_feeds(args))
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,3 @@
1
+ from .enricher import enrich
2
+ from .feeds import load_kev, load_epss, update_feeds
3
+ __all__ = ["enrich", "load_kev", "load_epss", "update_feeds"]
@@ -0,0 +1,33 @@
1
+ """
2
+ vulnpilot/enrich/enricher.py
3
+ Applies KEV and EPSS data to parsed findings.
4
+ """
5
+ from __future__ import annotations
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+ from vulnpilot.parser.nessus import Finding
10
+ from .feeds import load_kev, load_epss
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def enrich(findings: List[Finding], kev_path: Optional[Path] = None, epss_path: Optional[Path] = None) -> List[Finding]:
16
+ kev_set = load_kev(kev_path)
17
+ epss_map = load_epss(epss_path)
18
+ kev_hits = epss_hits = 0
19
+
20
+ for f in findings:
21
+ for cve in f.cve_list:
22
+ if cve in kev_set:
23
+ f.kev_match = True
24
+ kev_hits += 1
25
+ if cve in epss_map:
26
+ score, percentile = epss_map[cve]
27
+ if f.epss_score is None or score > f.epss_score:
28
+ f.epss_score = score
29
+ f.epss_percentile = percentile
30
+ epss_hits += 1
31
+
32
+ logger.info("Enrichment: %d KEV matches, %d EPSS scores", kev_hits, epss_hits)
33
+ return findings
@@ -0,0 +1,72 @@
1
+ """
2
+ vulnpilot/enrich/feeds.py
3
+ Loads CISA KEV and FIRST EPSS data from local cache.
4
+ Customer vulnerability data never leaves their machine.
5
+ """
6
+ from __future__ import annotations
7
+ import csv
8
+ import gzip
9
+ import json
10
+ import logging
11
+ import urllib.request
12
+ from pathlib import Path
13
+ from typing import Dict, Optional, Set, Tuple
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ DEFAULT_CACHE = Path.home() / ".vulnpilot" / "feeds"
18
+ KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
19
+ EPSS_URL = "https://epss.cyentia.com/epss_scores-current.csv.gz"
20
+
21
+
22
+ def load_kev(kev_path: Optional[Path] = None) -> Set[str]:
23
+ path = kev_path or DEFAULT_CACHE / "known_exploited_vulnerabilities.json"
24
+ if not path.exists():
25
+ logger.warning("KEV file not found at %s — run 'vulnpilot update-feeds' first.", path)
26
+ return set()
27
+ with open(path, encoding="utf-8") as fh:
28
+ data = json.load(fh)
29
+ cve_set = {v["cveID"].upper() for v in data.get("vulnerabilities", [])}
30
+ logger.info("Loaded %d KEV entries", len(cve_set))
31
+ return cve_set
32
+
33
+
34
+ def load_epss(epss_path: Optional[Path] = None) -> Dict[str, Tuple[float, float]]:
35
+ path = epss_path or DEFAULT_CACHE / "epss_scores-current.csv.gz"
36
+ if not path.exists():
37
+ uncompressed = path.with_suffix("")
38
+ if uncompressed.exists():
39
+ path = uncompressed
40
+ else:
41
+ logger.warning("EPSS file not found at %s — run 'vulnpilot update-feeds' first.", path)
42
+ return {}
43
+
44
+ epss: Dict[str, Tuple[float, float]] = {}
45
+ opener = gzip.open if path.suffix == ".gz" else open
46
+
47
+ with opener(path, "rt", encoding="utf-8") as fh:
48
+ for line in fh:
49
+ if line.startswith("#"):
50
+ continue
51
+ break
52
+ reader = csv.DictReader(fh, fieldnames=["cve", "epss", "percentile"])
53
+ next(reader, None)
54
+ for row in reader:
55
+ cve = row["cve"].strip().upper()
56
+ try:
57
+ epss[cve] = (float(row["epss"].strip()), float(row["percentile"].strip()))
58
+ except (ValueError, KeyError):
59
+ continue
60
+
61
+ logger.info("Loaded %d EPSS scores", len(epss))
62
+ return epss
63
+
64
+
65
+ def update_feeds(cache_dir: Optional[Path] = None) -> None:
66
+ cache = cache_dir or DEFAULT_CACHE
67
+ cache.mkdir(parents=True, exist_ok=True)
68
+ print("Downloading CISA KEV feed...")
69
+ urllib.request.urlretrieve(KEV_URL, cache / "known_exploited_vulnerabilities.json")
70
+ print("Downloading FIRST EPSS feed...")
71
+ urllib.request.urlretrieve(EPSS_URL, cache / "epss_scores-current.csv.gz")
72
+ print("Feeds updated successfully.")
@@ -0,0 +1,2 @@
1
+ from .nessus import parse_nessus_csv, Finding
2
+ __all__ = ["parse_nessus_csv", "Finding"]
@@ -0,0 +1,139 @@
1
+ """
2
+ vulnpilot/parser/nessus.py
3
+ Parses Nessus CSV exports into normalized Finding objects.
4
+ """
5
+ from __future__ import annotations
6
+ import csv
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import List, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ NESSUS_COLUMNS = {
15
+ "plugin id": "plugin_id",
16
+ "cve": "cve",
17
+ "cvss v3.0 base score": "cvss_v3",
18
+ "cvss v2.0 base score": "cvss_v2",
19
+ "risk": "risk",
20
+ "host": "host",
21
+ "protocol": "protocol",
22
+ "port": "port",
23
+ "name": "name",
24
+ "synopsis": "synopsis",
25
+ "description": "description",
26
+ "solution": "solution",
27
+ "see also": "references",
28
+ "plugin output": "plugin_output",
29
+ }
30
+
31
+ RISK_ORDER = {"critical": 4, "high": 3, "medium": 2, "low": 1, "none": 0, "": 0}
32
+
33
+
34
+ @dataclass
35
+ class Finding:
36
+ plugin_id: str
37
+ cve: str
38
+ cvss_v3: Optional[float]
39
+ cvss_v2: Optional[float]
40
+ risk: str
41
+ host: str
42
+ port: str
43
+ protocol: str
44
+ name: str
45
+ synopsis: str
46
+ description: str
47
+ solution: str
48
+ references: str
49
+ plugin_output: str
50
+ epss_score: Optional[float] = None
51
+ epss_percentile: Optional[float] = None
52
+ kev_match: bool = False
53
+ priority_score: Optional[float] = None
54
+ priority_label: Optional[str] = None
55
+
56
+ @property
57
+ def cve_list(self) -> List[str]:
58
+ if not self.cve:
59
+ return []
60
+ return [c.strip().upper() for c in self.cve.split(",") if c.strip().startswith("CVE-")]
61
+
62
+ @property
63
+ def cvss(self) -> float:
64
+ return self.cvss_v3 or self.cvss_v2 or 0.0
65
+
66
+ @property
67
+ def risk_value(self) -> int:
68
+ return RISK_ORDER.get(self.risk.lower(), 0)
69
+
70
+
71
+ def _safe_float(value: str) -> Optional[float]:
72
+ try:
73
+ return float(value.strip()) if value.strip() else None
74
+ except ValueError:
75
+ return None
76
+
77
+
78
+ def parse_nessus_csv(path: Path) -> List[Finding]:
79
+ path = Path(path)
80
+ if not path.exists():
81
+ raise FileNotFoundError(f"CSV not found: {path}")
82
+
83
+ with open(path, newline="", encoding="utf-8-sig") as fh:
84
+ raw = fh.read()
85
+
86
+ lines = raw.splitlines()
87
+ header_idx = None
88
+ for i, line in enumerate(lines):
89
+ lower = line.lower()
90
+ if "plugin id" in lower and "risk" in lower and "host" in lower:
91
+ header_idx = i
92
+ break
93
+
94
+ if header_idx is None:
95
+ raise ValueError(
96
+ "This does not look like a Nessus CSV export. "
97
+ "Expected columns: Plugin ID, Risk, Host, CVE."
98
+ )
99
+
100
+ body = "\n".join(lines[header_idx:])
101
+ reader = csv.DictReader(body.splitlines())
102
+
103
+ col_map = {}
104
+ for raw_col in (reader.fieldnames or []):
105
+ normalized = NESSUS_COLUMNS.get(raw_col.strip().lower())
106
+ if normalized:
107
+ col_map[raw_col] = normalized
108
+
109
+ findings: List[Finding] = []
110
+ skipped = 0
111
+
112
+ for row in reader:
113
+ mapped = {v: row.get(k, "").strip() for k, v in col_map.items()}
114
+ if mapped.get("risk", "").lower() in ("none", ""):
115
+ skipped += 1
116
+ continue
117
+ host = mapped.get("host", "")
118
+ if not host:
119
+ skipped += 1
120
+ continue
121
+ findings.append(Finding(
122
+ plugin_id=mapped.get("plugin_id", ""),
123
+ cve=mapped.get("cve", ""),
124
+ cvss_v3=_safe_float(mapped.get("cvss_v3", "")),
125
+ cvss_v2=_safe_float(mapped.get("cvss_v2", "")),
126
+ risk=mapped.get("risk", ""),
127
+ host=host,
128
+ port=mapped.get("port", ""),
129
+ protocol=mapped.get("protocol", ""),
130
+ name=mapped.get("name", ""),
131
+ synopsis=mapped.get("synopsis", ""),
132
+ description=mapped.get("description", ""),
133
+ solution=mapped.get("solution", ""),
134
+ references=mapped.get("references", ""),
135
+ plugin_output=mapped.get("plugin_output", ""),
136
+ ))
137
+
138
+ logger.info("Parsed %d findings (%d informational skipped)", len(findings), skipped)
139
+ return findings
@@ -0,0 +1,2 @@
1
+ from .terminal import render_summary, render_findings, render_top_hosts, render_free_tier_gate
2
+ __all__ = ["render_summary", "render_findings", "render_top_hosts", "render_free_tier_gate"]
@@ -0,0 +1,88 @@
1
+ """
2
+ vulnpilot/reports/terminal.py
3
+ Renders prioritized findings to terminal output.
4
+ """
5
+ from __future__ import annotations
6
+ from collections import Counter, defaultdict
7
+ from typing import List
8
+ from vulnpilot.parser.nessus import Finding
9
+
10
+ RESET="\033[0m";BOLD="\033[1m";RED="\033[91m";ORANGE="\033[33m"
11
+ YELLOW="\033[93m";CYAN="\033[96m";GREY="\033[90m";WHITE="\033[97m"
12
+
13
+ def _c(text, colour, use_colour):
14
+ return f"{colour}{text}{RESET}" if use_colour else text
15
+
16
+ def _label_colour(label, use_colour):
17
+ colours = {"CRITICAL NOW": RED, "HIGH": ORANGE, "MEDIUM": YELLOW, "LOW": CYAN}
18
+ return _c(label, colours.get(label, WHITE), use_colour)
19
+
20
+ def render_summary(findings: List[Finding], use_colour: bool = True) -> str:
21
+ total = len(findings)
22
+ risk_counts = Counter(f.risk.upper() for f in findings)
23
+ kev_count = sum(1 for f in findings if f.kev_match)
24
+ high_epss = sum(1 for f in findings if (f.epss_score or 0) >= 0.9)
25
+ unique_hosts = len({f.host for f in findings})
26
+ lines = [
27
+ "", _c("━"*60, BOLD, use_colour),
28
+ _c(" VulnPilot by PatchVex — Vulnerability Prioritization", BOLD, use_colour),
29
+ _c("━"*60, BOLD, use_colour), "",
30
+ f" Total findings : {_c(str(total), BOLD, use_colour)}",
31
+ f" Unique hosts : {unique_hosts}", "",
32
+ f" {_c('Critical', RED, use_colour)} : {risk_counts.get('CRITICAL', 0)}",
33
+ f" {_c('High', ORANGE, use_colour)} : {risk_counts.get('HIGH', 0)}",
34
+ f" {_c('Medium', YELLOW, use_colour)} : {risk_counts.get('MEDIUM', 0)}",
35
+ f" {_c('Low', CYAN, use_colour)} : {risk_counts.get('LOW', 0)}", "",
36
+ f" {_c('KEV matches', RED, use_colour)} (exploited now) : {_c(str(kev_count), BOLD, use_colour)}",
37
+ f" {_c('EPSS >= 90%', ORANGE, use_colour)} (high risk) : {high_epss}",
38
+ "", _c("━"*60, BOLD, use_colour),
39
+ ]
40
+ return "\n".join(lines)
41
+
42
+ def render_findings(findings: List[Finding], limit: int = 20, use_colour: bool = True) -> str:
43
+ shown = findings[:limit]
44
+ lines = ["", _c(f" TOP {limit} PRIORITIZED FINDINGS", BOLD, use_colour),
45
+ _c(" (KEV + EPSS + CVSS composite score)", GREY, use_colour), ""]
46
+ header = f" {'#':<5}{'Score':<7}{'Priority':<14}{'Host':<22}{'CVE':<20}{'Finding':<36}"
47
+ lines.append(_c(header, BOLD, use_colour))
48
+ lines.append(_c(" " + "─"*100, GREY, use_colour))
49
+ for i, f in enumerate(shown, start=1):
50
+ cve_display = f.cve_list[0] if f.cve_list else "N/A"
51
+ name_display = f.name[:34] + ".." if len(f.name) > 36 else f.name
52
+ host_display = f.host[:20] + ".." if len(f.host) > 22 else f.host
53
+ kev_flag = _c(" ★KEV", RED, use_colour) if f.kev_match else ""
54
+ row = (f" {i:<5}{f.priority_score:<7.1f}"
55
+ f"{(f.priority_label or ''):<14}{host_display:<22}"
56
+ f"{cve_display:<20}{name_display}{kev_flag}")
57
+ lines.append(row)
58
+ lines += ["", _c(" ★ KEV = CISA Known Exploited — patch these first, no debate.", RED, use_colour), ""]
59
+ return "\n".join(lines)
60
+
61
+ def render_top_hosts(findings: List[Finding], top_n: int = 10, use_colour: bool = True) -> str:
62
+ host_scores: dict = defaultdict(float)
63
+ host_kev: dict = defaultdict(int)
64
+ host_critical: dict = defaultdict(int)
65
+ for f in findings:
66
+ host_scores[f.host] += f.priority_score or 0
67
+ if f.kev_match: host_kev[f.host] += 1
68
+ if f.risk.lower() == "critical": host_critical[f.host] += 1
69
+ ranked = sorted(host_scores.items(), key=lambda x: x[1], reverse=True)[:top_n]
70
+ lines = ["", _c(f" TOP {top_n} HOSTS BY AGGREGATE RISK", BOLD, use_colour), ""]
71
+ for i, (host, score) in enumerate(ranked, start=1):
72
+ kev_tag = _c(f" [{host_kev[host]} KEV]", RED, use_colour) if host_kev[host] else ""
73
+ crit_tag = f" [{host_critical[host]} critical]" if host_critical[host] else ""
74
+ lines.append(f" {i:>2}. {host:<32} score={score:.1f}{kev_tag}{crit_tag}")
75
+ lines.append("")
76
+ return "\n".join(lines)
77
+
78
+ def render_free_tier_gate(total: int, shown: int, use_colour: bool = True) -> str:
79
+ lines = [
80
+ "", _c("─"*60, GREY, use_colour),
81
+ _c(f" FREE TIER: showing top {shown} of {total} findings", YELLOW, use_colour),
82
+ _c(f" {total - shown} findings hidden.", YELLOW, use_colour), "",
83
+ " Upgrade to VulnPilot Professional to unlock all findings,",
84
+ " PDF reports, Jira integration, and scheduled scans.", "",
85
+ _c(" https://patchvex.com/pricing", CYAN, use_colour),
86
+ _c("─"*60, GREY, use_colour), "",
87
+ ]
88
+ return "\n".join(lines)
@@ -0,0 +1,2 @@
1
+ from .engine import score_all, score_finding, ScoringConfig, DEFAULT_CONFIG
2
+ __all__ = ["score_all", "score_finding", "ScoringConfig", "DEFAULT_CONFIG"]
@@ -0,0 +1,60 @@
1
+ """
2
+ vulnpilot/scoring/engine.py
3
+ Composite risk scoring engine.
4
+ Score = KEV(40) + EPSS(35) + CVSS(15) + Severity(10)
5
+ KEV hard floor: any KEV finding scores at least 75.
6
+ """
7
+ from __future__ import annotations
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import List
11
+ from vulnpilot.parser.nessus import Finding
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ SEVERITY_SCORE = {"critical": 1.0, "high": 0.75, "medium": 0.4, "low": 0.1, "none": 0.0, "": 0.0}
16
+
17
+
18
+ @dataclass
19
+ class ScoringConfig:
20
+ kev_weight: float = 40.0
21
+ epss_weight: float = 35.0
22
+ cvss_weight: float = 15.0
23
+ severity_weight: float = 10.0
24
+
25
+
26
+ DEFAULT_CONFIG = ScoringConfig()
27
+
28
+
29
+ def _normalize_cvss(cvss: float) -> float:
30
+ return min(max(cvss, 0.0), 10.0) / 10.0
31
+
32
+
33
+ def score_finding(finding: Finding, config: ScoringConfig = DEFAULT_CONFIG) -> float:
34
+ raw = (
35
+ (config.kev_weight if finding.kev_match else 0.0) +
36
+ config.epss_weight * (finding.epss_score or 0.0) +
37
+ config.cvss_weight * _normalize_cvss(finding.cvss) +
38
+ config.severity_weight * SEVERITY_SCORE.get(finding.risk.lower(), 0.0)
39
+ )
40
+ if finding.kev_match:
41
+ raw = max(raw, 75.0)
42
+ return min(round(raw, 2), 100.0)
43
+
44
+
45
+ def _priority_label(score: float, kev: bool) -> str:
46
+ if kev or score >= 75:
47
+ return "CRITICAL NOW"
48
+ if score >= 50:
49
+ return "HIGH"
50
+ if score >= 25:
51
+ return "MEDIUM"
52
+ return "LOW"
53
+
54
+
55
+ def score_all(findings: List[Finding], config: ScoringConfig = DEFAULT_CONFIG, limit: int = 0) -> List[Finding]:
56
+ for f in findings:
57
+ f.priority_score = score_finding(f, config)
58
+ f.priority_label = _priority_label(f.priority_score, f.kev_match)
59
+ findings.sort(key=lambda f: f.priority_score, reverse=True)
60
+ return findings[:limit] if limit > 0 else findings
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PatchVex
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.1
2
+ Name: vulnpilot
3
+ Version: 0.1.0
4
+ Summary: Turn vulnerability scan data into prioritized action. Runs locally. Your data never leaves your machine.
5
+ Project-URL: Homepage, https://patchvex.com
6
+ Project-URL: Repository, https://github.com/PatchVex/vulnpilot
7
+ Project-URL: Bug Tracker, https://github.com/PatchVex/vulnpilot/issues
8
+ Keywords: security,vulnerability,nessus,cve,kev,epss,devsecops
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Topic :: Security
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+
24
+ # VulnPilot
25
+
26
+ **Prioritize vulnerabilities using real-world exploit intelligence — not just severity scores.**
27
+
28
+ Runs locally. Your data never leaves your machine.
29
+
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
31
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://python.org)
32
+ [![Status: Developer Preview](https://img.shields.io/badge/status-developer%20preview-orange.svg)]()
33
+ [![Version: 0.1.0](https://img.shields.io/badge/version-0.1.0-blue.svg)]()
34
+
35
+ ---
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ pip install vulnpilot
41
+ vulnpilot update-feeds
42
+ vulnpilot analyze scan.csv
43
+ ```
44
+
45
+ VulnPilot downloads the latest public threat intelligence, analyzes your Nessus scan locally, and shows what should be remediated first. No API keys required.
46
+
47
+ ---
48
+
49
+ ## Features
50
+
51
+ - Local-first vulnerability prioritization — scan data never leaves your machine
52
+ - CISA KEV enrichment — flags findings confirmed exploited in the wild
53
+ - FIRST EPSS enrichment — exploitation probability scoring
54
+ - Composite risk scoring — KEV + EPSS + CVSS combined
55
+ - Top vulnerable hosts ranked by aggregate risk
56
+ - Zero cloud upload, zero telemetry, zero account required
57
+ - GitHub Actions daily feed automation
58
+ - Open source Community Edition — MIT licensed
59
+
60
+ ---
61
+
62
+ ## The problem
63
+
64
+ Security teams often spend hours manually triaging scan results. Your Nessus export contains thousands of findings. CVSS says hundreds are Critical. The real question — which ones are actively being exploited right now?
65
+
66
+ VulnPilot automates this process in seconds on typical scan files.
67
+
68
+ ---
69
+
70
+ ## Why VulnPilot?
71
+
72
+ | Instead of | VulnPilot |
73
+ |---|---|
74
+ | Sorting by CVSS score alone | Uses KEV + EPSS + CVSS composite scoring |
75
+ | Manual triage taking hours | Automated prioritization in seconds |
76
+ | Uploading scans to cloud services | Local-first — data never leaves your machine |
77
+ | Enterprise-only platforms | Developer Preview — free and open source |
78
+
79
+ ---
80
+
81
+ ## Why local-first?
82
+
83
+ Many organizations prohibit uploading vulnerability scan data to third-party cloud services. VulnPilot performs all analysis locally on your machine.
84
+
85
+ No customer vulnerability data is transmitted outside your environment.
86
+
87
+ ---
88
+
89
+ ## Architecture
90
+
91
+ ```
92
+ Public Threat Intelligence
93
+ +-------------------------------+
94
+ | CISA KEV FIRST EPSS |
95
+ +---------------+---------------+
96
+ |
97
+ vulnpilot update-feeds
98
+ |
99
+ ~/.vulnpilot/feeds/ (local cache)
100
+ |
101
+ vulnpilot analyze
102
+ |
103
+ Nessus CSV (Local Machine Only)
104
+ |
105
+ Composite Risk Engine
106
+ |
107
+ Prioritized Findings
108
+ ```
109
+
110
+ Only public threat intelligence feeds are downloaded. No API keys required. Your scan data never leaves your machine.
111
+
112
+ ---
113
+
114
+ ## Install
115
+
116
+ ```bash
117
+ pip install vulnpilot
118
+ ```
119
+
120
+ Tested on Python 3.10, 3.11, and 3.12.
121
+
122
+ ---
123
+
124
+ ## Usage
125
+
126
+ ```bash
127
+ # Download latest KEV and EPSS feeds
128
+ vulnpilot update-feeds
129
+
130
+ # Analyze a Nessus CSV export
131
+ vulnpilot analyze scan.csv
132
+
133
+ # Show top N hosts by aggregate risk
134
+ vulnpilot analyze scan.csv --top-hosts 5
135
+
136
+ # Use local feed files
137
+ vulnpilot analyze scan.csv --kev ./kev.json --epss ./epss.csv.gz
138
+
139
+ # Disable colour output (for CI pipelines)
140
+ vulnpilot analyze scan.csv --no-colour
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Example output
146
+
147
+ ```
148
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
149
+ VulnPilot by PatchVex — Vulnerability Prioritization
150
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
151
+ Total findings : 5,482
152
+ Unique hosts : 47
153
+ Critical : 142
154
+ KEV matches : 19
155
+ EPSS >= 90% : 31
156
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157
+
158
+ # Score Priority Host CVE Finding
159
+ ───────────────────────────────────────────────────────────────────────
160
+ 1 100.0 CRITICAL NOW 192.168.1.10 CVE-2021-44228 Log4Shell ★KEV
161
+ 2 100.0 CRITICAL NOW 192.168.1.25 CVE-2023-34362 MOVEit SQL Injection ★KEV
162
+ 3 99.8 CRITICAL NOW 192.168.1.15 CVE-2020-1472 Zerologon ★KEV
163
+ 4 99.7 CRITICAL NOW 192.168.1.11 CVE-2021-26084 Confluence RCE ★KEV
164
+ 5 11.5 LOW 192.168.1.10 N/A SSH Weak Ciphers
165
+
166
+ ★ KEV = CISA Known Exploited Vulnerability — highest remediation priority
167
+ based on active exploitation in the wild.
168
+
169
+ TOP 10 HOSTS BY AGGREGATE RISK
170
+ 1. 192.168.1.10 score=122.0 [1 KEV] [1 critical]
171
+ 2. 192.168.1.25 score=100.0 [1 KEV] [1 critical]
172
+ 3. 192.168.1.15 score=99.8 [1 KEV] [1 critical]
173
+ ```
174
+
175
+ ---
176
+
177
+ ## How scoring works
178
+
179
+ The scoring algorithm is deterministic, transparent, and fully documented.
180
+
181
+ VulnPilot uses a composite risk score that combines four signals:
182
+
183
+ | Signal | Weight | Source |
184
+ |---|---|---|
185
+ | CISA KEV match | 40% | Known exploited in the wild |
186
+ | FIRST EPSS score | 35% | Exploitation probability |
187
+ | CVSS base score | 15% | Severity context |
188
+ | Scanner risk rating | 10% | Nessus severity label |
189
+
190
+ The composite score is intentionally opinionated. Known exploited vulnerabilities receive the greatest weight because active exploitation is a stronger predictor of remediation priority than severity alone. EPSS estimates exploitation likelihood in the next 30 days, while CVSS and scanner severity provide additional context for findings without EPSS data.
191
+
192
+ Any finding confirmed in the CISA KEV catalog scores a minimum of 75 regardless of other factors. The weighting model is intentionally transparent and may evolve based on community feedback and real-world usage.
193
+
194
+ > **Note**
195
+ >
196
+ > VulnPilot provides prioritization guidance to assist remediation workflows.
197
+ > Final remediation decisions should always consider asset criticality, business context,
198
+ > exploit mitigations, and organizational risk tolerance.
199
+
200
+ ---
201
+
202
+ ## Privacy by design
203
+
204
+ - Scan data processed entirely on your local machine
205
+ - No account required
206
+ - No cloud upload, ever
207
+ - No telemetry or analytics
208
+ - No API keys required
209
+ - Works air-gapped after initial feed download
210
+ - Open source — inspect every line of code
211
+
212
+ ---
213
+
214
+ ## Feed updates
215
+
216
+ VulnPilot pulls two public datasets:
217
+
218
+ - **CISA KEV** — Known Exploited Vulnerabilities catalog (maintained by CISA)
219
+ - **FIRST EPSS** — Exploit Prediction Scoring System (updated daily by FIRST.org)
220
+
221
+ Feeds are cached at `~/.vulnpilot/feeds/` on your machine. No API keys required.
222
+
223
+ ```bash
224
+ vulnpilot update-feeds
225
+ ```
226
+
227
+ The GitHub repository also runs an automated daily feed sync via GitHub Actions, publishing optimized feed files for the CLI to consume.
228
+
229
+ ---
230
+
231
+ ## Supported scanners
232
+
233
+ | Scanner | Status |
234
+ |---|---|
235
+ | Nessus (.csv export) | ✅ Supported |
236
+ | Qualys | Planned |
237
+ | Rapid7 | Planned |
238
+ | OpenVAS | Planned |
239
+ | Microsoft Defender | Planned |
240
+ | AWS Inspector | Planned |
241
+
242
+ ---
243
+
244
+ ## Roadmap
245
+
246
+ **Current — v0.1.0 (Developer Preview)**
247
+ - [x] Nessus CSV parser
248
+ - [x] CISA KEV enrichment
249
+ - [x] FIRST EPSS enrichment
250
+ - [x] Composite risk scoring
251
+ - [x] Prioritized terminal output
252
+ - [x] Top hosts by aggregate risk
253
+ - [x] Free tier — top 20 findings
254
+ - [x] GitHub Actions daily feed automation
255
+
256
+ **v0.2.0**
257
+ - [ ] HTML report export
258
+ - [ ] PDF report export
259
+
260
+ **v0.3.0**
261
+ - [ ] Jira integration
262
+ - [ ] Slack notifications
263
+ - [ ] Scheduled scans
264
+
265
+ **v0.4.0**
266
+ - [ ] Qualys CSV support
267
+ - [ ] REST API
268
+
269
+ **v1.0.0**
270
+ - [ ] Rapid7 and OpenVAS support
271
+ - [ ] Self-hosted Docker edition
272
+ - [ ] Team features
273
+
274
+ Future development priorities will be driven by community feedback and real-world usage.
275
+
276
+ ---
277
+
278
+ ## Requirements
279
+
280
+ - Python 3.10, 3.11, or 3.12
281
+ - pip
282
+ - Internet connection for feed updates (air-gapped use supported after initial download)
283
+ - Nessus .csv export file
284
+
285
+ ---
286
+
287
+ ## Contributing
288
+
289
+ Issues, bug reports, and pull requests are welcome.
290
+
291
+ - **Bug reports and feature requests:** [github.com/PatchVex/vulnpilot/issues](https://github.com/PatchVex/vulnpilot/issues)
292
+ - **Security disclosures:** security@patchvex.com
293
+
294
+ Good first issues are labelled `good first issue` in the issue tracker. Please search existing issues before opening a new one.
295
+
296
+ ---
297
+
298
+ ## Acknowledgements
299
+
300
+ VulnPilot uses publicly available threat intelligence published by:
301
+
302
+ - [CISA Known Exploited Vulnerabilities Catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)
303
+ - [FIRST Exploit Prediction Scoring System (EPSS)](https://www.first.org/epss/)
304
+
305
+ Thank you to both organizations for maintaining these community resources.
306
+
307
+ ---
308
+
309
+ ## License
310
+
311
+ MIT License — see [LICENSE](LICENSE) for details.
312
+
313
+ Free to use, modify, and distribute. Commercial use permitted.
314
+
315
+ ---
316
+
317
+ ## About PatchVex
318
+
319
+ VulnPilot is built and maintained by [PatchVex](https://patchvex.com).
320
+
321
+ PatchVex builds privacy-first workflow tools for security and DevSecOps teams. Our products help engineers spend less time managing vulnerability data and more time fixing the issues that matter.
322
+
323
+ - Website: [patchvex.com](https://patchvex.com)
324
+ - Email: hello@patchvex.com
325
+ - GitHub: [github.com/PatchVex](https://github.com/PatchVex)
@@ -0,0 +1,17 @@
1
+ vulnpilot/__init__.py,sha256=pfuSXvGCERnVj-EYz3ZNd0VuRjnJ8kFK5pXlXwpqG8s,131
2
+ vulnpilot/cli.py,sha256=P7XnBzJiNehX-jwzmVjaKyIcyqfwx61Aw9SBcZ0fD4o,3499
3
+ vulnpilot/enrich/__init__.py,sha256=S37CGy1-A-vPiJnTRESGxTrfDOu5E75bRI8ZGP0FaIs,144
4
+ vulnpilot/enrich/enricher.py,sha256=iM0yCQrY16P2f8XvvkPjEPnJGAHBPA3YjsqFzhZDW6c,1063
5
+ vulnpilot/enrich/feeds.py,sha256=tJ0ctJqd6sln_yCFBSGCWXx6BDbQkNjdkh5xAb1kGI4,2634
6
+ vulnpilot/parser/__init__.py,sha256=rVfaYC2pXjejQyge5rKQ0qFnjqxb9wyxsYrm1TB8ME8,88
7
+ vulnpilot/parser/nessus.py,sha256=Grb8i6ycjXVc06U6jAazWsrbxDD6hip6MjcRvu_-uJA,4123
8
+ vulnpilot/reports/__init__.py,sha256=5el470lZD9H7o4Inbb_Kx0BI35y72Pqqo8p07scAFRA,188
9
+ vulnpilot/reports/terminal.py,sha256=m7E4pJeyd36is--U2tY4assuT5eAlBhx2-YLIWg4GOY,4643
10
+ vulnpilot/scoring/__init__.py,sha256=Ss9K9exGRYqDolkxTffvO3nLFzgtZi7CU2kwjwy5zyw,152
11
+ vulnpilot/scoring/engine.py,sha256=vvU43x48cXZYzmUA1ahHaZRcNFi2Eze2RiDJC8DQSoU,1797
12
+ vulnpilot-0.1.0.dist-info/LICENSE,sha256=RK6YHCN0U9EnI1NRO6mY1HANMOw0xFmgx46cem-yy0w,1065
13
+ vulnpilot-0.1.0.dist-info/METADATA,sha256=minRidZNleYG1JaIp2EGBqWEEnPFyrWCKUsrMFkDDIc,10534
14
+ vulnpilot-0.1.0.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
15
+ vulnpilot-0.1.0.dist-info/entry_points.txt,sha256=Kn41I6BzrmRvtjcmn09hEI1EUMn14zLTAkwDxeBu7IM,49
16
+ vulnpilot-0.1.0.dist-info/top_level.txt,sha256=B0edhMLTMuc7VDzyPUkcjGgisM-tPbZQ5ZWp3yj8aN8,10
17
+ vulnpilot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (71.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vulnpilot = vulnpilot.cli:main
@@ -0,0 +1 @@
1
+ vulnpilot