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()
|