threatpulse 0.1.0__tar.gz

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.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: threatpulse
3
+ Version: 0.1.0
4
+ Summary: Scan your dependencies for weaponized vulnerabilities. Powered by ThreatPulse x402.
5
+ Project-URL: Homepage, https://threatpulse.waltsoft.net
6
+ Project-URL: Repository, https://github.com/awsdataarchitect/threatpulse-cli
7
+ Author-email: WaltSoft <vivek@waltsoft.net>
8
+ License: MIT
9
+ Keywords: cve,devsecops,exploit,sbom,security,vulnerability
10
+ Classifier: Environment :: Console
11
+ Classifier: Topic :: Security
12
+ Classifier: Topic :: Software Development :: Quality Assurance
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: click>=8.0
15
+ Requires-Dist: requests>=2.28
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ThreatPulse CLI
19
+
20
+ Scan your dependencies for **weaponized** vulnerabilities. Powered by [threatpulse.waltsoft.net](https://threatpulse.waltsoft.net).
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install threatpulse
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Scan a lockfile
32
+ threatpulse scan --file package-lock.json
33
+
34
+ # Fail CI if urgency >= 80
35
+ threatpulse scan --threshold 80
36
+
37
+ # JSON output for piping
38
+ threatpulse scan --format json | jq '.[] | select(.urgency_score > 70)'
39
+
40
+ # SARIF for GitHub Code Scanning
41
+ threatpulse scan --format sarif > results.sarif
42
+ ```
43
+
44
+ ## What makes this different
45
+
46
+ Unlike Trivy/Snyk/Inspector, ThreatPulse tells you if a CVE is **actively weaponized**:
47
+
48
+ ```
49
+ šŸ”“ CVE-2024-45257 HIGH weaponized 95 metasploit:exploit/unix/webapp/byob_unauth_rce
50
+ 🟔 CVE-2025-1234 MEDIUM poc 45 github.com/user/CVE-2025-1234
51
+ 🟢 CVE-2025-5678 LOW none 12 no known exploit
52
+ ```
53
+
54
+ ## Supported lockfiles
55
+
56
+ - `package-lock.json` (npm)
57
+ - `requirements.txt` (pip)
58
+ - `Cargo.lock` (Rust)
59
+ - `go.sum` (Go)
60
+ - `Gemfile.lock` (Ruby)
61
+
62
+ ## GitHub Action
63
+
64
+ ```yaml
65
+ - uses: awsdataarchitect/threatpulse-action@v1
66
+ with:
67
+ fail-on-urgency: 80
68
+ ```
69
+
70
+ ## Links
71
+
72
+ - API: https://threatpulse.waltsoft.net
73
+ - GitHub: https://github.com/awsdataarchitect/threatpulse-cli
@@ -0,0 +1,56 @@
1
+ # ThreatPulse CLI
2
+
3
+ Scan your dependencies for **weaponized** vulnerabilities. Powered by [threatpulse.waltsoft.net](https://threatpulse.waltsoft.net).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install threatpulse
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Scan a lockfile
15
+ threatpulse scan --file package-lock.json
16
+
17
+ # Fail CI if urgency >= 80
18
+ threatpulse scan --threshold 80
19
+
20
+ # JSON output for piping
21
+ threatpulse scan --format json | jq '.[] | select(.urgency_score > 70)'
22
+
23
+ # SARIF for GitHub Code Scanning
24
+ threatpulse scan --format sarif > results.sarif
25
+ ```
26
+
27
+ ## What makes this different
28
+
29
+ Unlike Trivy/Snyk/Inspector, ThreatPulse tells you if a CVE is **actively weaponized**:
30
+
31
+ ```
32
+ šŸ”“ CVE-2024-45257 HIGH weaponized 95 metasploit:exploit/unix/webapp/byob_unauth_rce
33
+ 🟔 CVE-2025-1234 MEDIUM poc 45 github.com/user/CVE-2025-1234
34
+ 🟢 CVE-2025-5678 LOW none 12 no known exploit
35
+ ```
36
+
37
+ ## Supported lockfiles
38
+
39
+ - `package-lock.json` (npm)
40
+ - `requirements.txt` (pip)
41
+ - `Cargo.lock` (Rust)
42
+ - `go.sum` (Go)
43
+ - `Gemfile.lock` (Ruby)
44
+
45
+ ## GitHub Action
46
+
47
+ ```yaml
48
+ - uses: awsdataarchitect/threatpulse-action@v1
49
+ with:
50
+ fail-on-urgency: 80
51
+ ```
52
+
53
+ ## Links
54
+
55
+ - API: https://threatpulse.waltsoft.net
56
+ - GitHub: https://github.com/awsdataarchitect/threatpulse-cli
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "threatpulse"
3
+ version = "0.1.0"
4
+ description = "Scan your dependencies for weaponized vulnerabilities. Powered by ThreatPulse x402."
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = {text = "MIT"}
8
+ authors = [{name = "WaltSoft", email = "vivek@waltsoft.net"}]
9
+ dependencies = ["requests>=2.28", "click>=8.0"]
10
+ keywords = ["security", "vulnerability", "cve", "exploit", "sbom", "devsecops"]
11
+ classifiers = [
12
+ "Topic :: Security",
13
+ "Topic :: Software Development :: Quality Assurance",
14
+ "Environment :: Console",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://threatpulse.waltsoft.net"
19
+ Repository = "https://github.com/awsdataarchitect/threatpulse-cli"
20
+
21
+ [project.scripts]
22
+ threatpulse = "threatpulse.cli:main"
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
@@ -0,0 +1 @@
1
+ """ThreatPulse — weaponized vulnerability intelligence for your pipeline."""
@@ -0,0 +1,163 @@
1
+ """ThreatPulse CLI — scan lockfiles for weaponized vulnerabilities."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+ import requests
9
+
10
+ API_BASE = "https://threatpulse.waltsoft.net"
11
+
12
+
13
+ def parse_lockfile(path: str) -> list[str]:
14
+ """Extract package names from lockfiles."""
15
+ p = Path(path)
16
+ content = p.read_text()
17
+
18
+ if p.name == "package-lock.json":
19
+ data = json.loads(content)
20
+ deps = data.get("packages", data.get("dependencies", {}))
21
+ return [k.split("node_modules/")[-1] for k in deps if k]
22
+
23
+ if p.name in ("requirements.txt", "constraints.txt"):
24
+ return [l.split("==")[0].split(">=")[0].split("~=")[0].strip()
25
+ for l in content.splitlines() if l.strip() and not l.startswith("#")]
26
+
27
+ if p.name == "Cargo.lock":
28
+ return [l.split('"')[1] for l in content.splitlines() if l.startswith('name = "')]
29
+
30
+ if p.name in ("go.sum", "go.mod"):
31
+ return [l.split()[0] for l in content.splitlines()
32
+ if l.strip() and not l.startswith("module") and not l.startswith("go ")]
33
+
34
+ if p.name == "Gemfile.lock":
35
+ return [l.strip().split()[0] for l in content.splitlines()
36
+ if l.startswith(" ") and not l.strip().startswith("(")]
37
+
38
+ # Fallback: one package per line
39
+ return [l.strip() for l in content.splitlines() if l.strip()]
40
+
41
+
42
+ def scan_packages(packages: list[str], threshold: int, payment_key: str | None) -> list[dict]:
43
+ """Call ThreatPulse /v1/scan endpoint."""
44
+ headers = {}
45
+ if payment_key:
46
+ headers["X-Payment-Proof"] = payment_key
47
+
48
+ resp = requests.post(f"{API_BASE}/v1/scan", json={"packages": packages}, headers=headers)
49
+
50
+ if resp.status_code == 402:
51
+ # Show pricing info for free tier users
52
+ click.echo("⚔ Free tier: showing cached results only. Set THREATPULSE_KEY for full access.", err=True)
53
+ return []
54
+
55
+ if resp.status_code != 200:
56
+ click.echo(f"Error: {resp.status_code} {resp.text}", err=True)
57
+ return []
58
+
59
+ return resp.json().get("vulnerabilities", [])
60
+
61
+
62
+ URGENCY_COLORS = {
63
+ "CRITICAL": "red",
64
+ "HIGH": "yellow",
65
+ "MEDIUM": "cyan",
66
+ "LOW": "green",
67
+ }
68
+
69
+ EXPLOIT_ICONS = {
70
+ "weaponized": "šŸ”“",
71
+ "poc": "🟔",
72
+ "none": "🟢",
73
+ }
74
+
75
+
76
+ @click.command()
77
+ @click.argument("path", default=".")
78
+ @click.option("--file", "-f", help="Lockfile path (auto-detected if not specified)")
79
+ @click.option("--threshold", "-t", default=0, help="Fail if any CVE urgency >= threshold")
80
+ @click.option("--format", "fmt", type=click.Choice(["table", "json", "sarif"]), default="table")
81
+ @click.option("--key", envvar="THREATPULSE_KEY", help="Payment key (or set THREATPULSE_KEY env)")
82
+ def main(path: str, file: str | None, threshold: int, fmt: str, key: str | None):
83
+ """Scan dependencies for weaponized vulnerabilities.
84
+
85
+ \b
86
+ Examples:
87
+ threatpulse scan .
88
+ threatpulse scan --file package-lock.json --threshold 80
89
+ threatpulse scan --format json | jq '.[] | select(.urgency_score > 70)'
90
+ """
91
+ # Find lockfile
92
+ if file:
93
+ lockfile = file
94
+ else:
95
+ p = Path(path)
96
+ candidates = ["package-lock.json", "yarn.lock", "requirements.txt",
97
+ "Cargo.lock", "go.sum", "Gemfile.lock", "pnpm-lock.yaml"]
98
+ lockfile = next((str(p / c) for c in candidates if (p / c).exists()), None)
99
+ if not lockfile:
100
+ click.echo("No lockfile found. Use --file to specify.", err=True)
101
+ sys.exit(1)
102
+
103
+ click.echo(f"šŸ“¦ Scanning {lockfile}...", err=True)
104
+ packages = parse_lockfile(lockfile)
105
+ click.echo(f" Found {len(packages)} packages", err=True)
106
+
107
+ vulns = scan_packages(packages, threshold, key)
108
+
109
+ if not vulns:
110
+ click.echo("āœ… No known vulnerabilities found.", err=True)
111
+ sys.exit(0)
112
+
113
+ # Sort by urgency
114
+ vulns.sort(key=lambda v: v.get("urgency_score", 0), reverse=True)
115
+
116
+ if fmt == "json":
117
+ click.echo(json.dumps(vulns, indent=2))
118
+ elif fmt == "sarif":
119
+ click.echo(json.dumps(to_sarif(vulns), indent=2))
120
+ else:
121
+ # Table output
122
+ click.echo(f"\n{'CVE':<20} {'Severity':<10} {'Exploit':<12} {'Urgency':<8} {'Package'}", err=True)
123
+ click.echo("─" * 75, err=True)
124
+ for v in vulns:
125
+ icon = EXPLOIT_ICONS.get(v.get("exploit_status", "none"), "⚪")
126
+ sev = v.get("severity", "?")
127
+ color = URGENCY_COLORS.get(sev, "white")
128
+ click.echo(
129
+ f"{icon} {v.get('cve_id', '?'):<18} "
130
+ f"{click.style(sev, fg=color):<19} "
131
+ f"{v.get('exploit_status', '?'):<12} "
132
+ f"{v.get('urgency_score', '?'):<8} "
133
+ f"{', '.join(v.get('affected_products', [])[:2])}"
134
+ , err=True)
135
+
136
+ # Exit code based on threshold
137
+ max_urgency = max((v.get("urgency_score", 0) for v in vulns), default=0)
138
+ if threshold > 0 and max_urgency >= threshold:
139
+ click.echo(f"\nāŒ FAILED: urgency {max_urgency} >= threshold {threshold}", err=True)
140
+ sys.exit(1)
141
+
142
+ click.echo(f"\nāš ļø {len(vulns)} vulnerabilities found (max urgency: {max_urgency})", err=True)
143
+
144
+
145
+ def to_sarif(vulns: list[dict]) -> dict:
146
+ """Convert to SARIF format for GitHub Code Scanning."""
147
+ return {
148
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
149
+ "version": "2.1.0",
150
+ "runs": [{
151
+ "tool": {"driver": {"name": "ThreatPulse", "version": "0.1.0",
152
+ "informationUri": "https://threatpulse.waltsoft.net"}},
153
+ "results": [{
154
+ "ruleId": v.get("cve_id", ""),
155
+ "level": "error" if v.get("urgency_score", 0) >= 70 else "warning",
156
+ "message": {"text": f"{v.get('cve_id')}: {v.get('exploit_status')} (urgency {v.get('urgency_score')})"},
157
+ } for v in vulns],
158
+ }],
159
+ }
160
+
161
+
162
+ if __name__ == "__main__":
163
+ main()