oneport-depcheck 0.4.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.
Files changed (39) hide show
  1. oneport_depcheck-0.4.0/PKG-INFO +8 -0
  2. oneport_depcheck-0.4.0/depcheck/__init__.py +5 -0
  3. oneport_depcheck-0.4.0/depcheck/audit_log.py +78 -0
  4. oneport_depcheck-0.4.0/depcheck/auth.py +207 -0
  5. oneport_depcheck-0.4.0/depcheck/cache.py +57 -0
  6. oneport_depcheck-0.4.0/depcheck/cicd.py +79 -0
  7. oneport_depcheck-0.4.0/depcheck/cli.py +748 -0
  8. oneport_depcheck-0.4.0/depcheck/container_scanner.py +246 -0
  9. oneport_depcheck-0.4.0/depcheck/dashboard.py +272 -0
  10. oneport_depcheck-0.4.0/depcheck/epss.py +64 -0
  11. oneport_depcheck-0.4.0/depcheck/github_advisory.py +95 -0
  12. oneport_depcheck-0.4.0/depcheck/hooks.py +195 -0
  13. oneport_depcheck-0.4.0/depcheck/integrations.py +221 -0
  14. oneport_depcheck-0.4.0/depcheck/license_checker.py +168 -0
  15. oneport_depcheck-0.4.0/depcheck/multi_repo.py +283 -0
  16. oneport_depcheck-0.4.0/depcheck/npm_scanner.py +117 -0
  17. oneport_depcheck-0.4.0/depcheck/nvd.py +107 -0
  18. oneport_depcheck-0.4.0/depcheck/policy.py +127 -0
  19. oneport_depcheck-0.4.0/depcheck/reachability.py +70 -0
  20. oneport_depcheck-0.4.0/depcheck/reporter.py +65 -0
  21. oneport_depcheck-0.4.0/depcheck/reporter_html.py +177 -0
  22. oneport_depcheck-0.4.0/depcheck/reporter_pdf.py +191 -0
  23. oneport_depcheck-0.4.0/depcheck/risk_scorer.py +129 -0
  24. oneport_depcheck-0.4.0/depcheck/sbom.py +93 -0
  25. oneport_depcheck-0.4.0/depcheck/sbom_diff.py +123 -0
  26. oneport_depcheck-0.4.0/depcheck/scanner.py +135 -0
  27. oneport_depcheck-0.4.0/depcheck/scheduler.py +132 -0
  28. oneport_depcheck-0.4.0/depcheck/slack_notify.py +137 -0
  29. oneport_depcheck-0.4.0/depcheck/supply_chain.py +146 -0
  30. oneport_depcheck-0.4.0/depcheck/suppress.py +164 -0
  31. oneport_depcheck-0.4.0/depcheck/transitive.py +87 -0
  32. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/PKG-INFO +8 -0
  33. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/SOURCES.txt +37 -0
  34. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/dependency_links.txt +1 -0
  35. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/entry_points.txt +2 -0
  36. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/requires.txt +4 -0
  37. oneport_depcheck-0.4.0/oneport_depcheck.egg-info/top_level.txt +1 -0
  38. oneport_depcheck-0.4.0/setup.cfg +4 -0
  39. oneport_depcheck-0.4.0/setup.py +18 -0
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: oneport-depcheck
3
+ Version: 0.4.0
4
+ Requires-Dist: requests
5
+ Requires-Dist: packaging
6
+ Requires-Dist: click
7
+ Requires-Dist: rich
8
+ Dynamic: requires-dist
@@ -0,0 +1,5 @@
1
+ __version__ = "0.4.0"
2
+ __author__ = "Bitan Dutta"
3
+ __email__ = "bitandutta6345@gmail.com"
4
+ __description__ = "MNC-grade dependency vulnerability scanner"
5
+ __url__ = "https://oneport.co.in"
@@ -0,0 +1,78 @@
1
+ import json
2
+ import os
3
+ from datetime import datetime, timezone
4
+
5
+ AUDIT_LOG_PATH = os.path.join(os.path.expanduser("~"), ".depcheck", "audit.jsonl")
6
+
7
+
8
+ def _ensure_dir():
9
+ os.makedirs(os.path.dirname(AUDIT_LOG_PATH), exist_ok=True)
10
+
11
+
12
+ def log_scan(
13
+ packages_scanned: int,
14
+ findings: list[dict],
15
+ supply_chain: list[dict],
16
+ license_issues: list[dict],
17
+ policy_violations: list[dict],
18
+ scan_target: str = "unknown",
19
+ extra: dict = None,
20
+ ) -> str:
21
+ """Append one scan record to the audit log. Returns the log path."""
22
+ _ensure_dir()
23
+
24
+ severity_counts = {}
25
+ for f in findings:
26
+ sev = f.get("severity", "UNKNOWN")
27
+ severity_counts[sev] = severity_counts.get(sev, 0) + 1
28
+
29
+ record = {
30
+ "timestamp": datetime.now(timezone.utc).isoformat(),
31
+ "scan_target": scan_target,
32
+ "packages_scanned": packages_scanned,
33
+ "findings_count": len(findings),
34
+ "severity_breakdown": severity_counts,
35
+ "supply_chain_count": len(supply_chain),
36
+ "license_issues_count": len(license_issues),
37
+ "policy_violations_count": len(policy_violations),
38
+ "cve_ids": [
39
+ cve for f in findings
40
+ for cve in f.get("ids", [])
41
+ if cve.startswith("CVE-")
42
+ ][:20],
43
+ "extra": extra or {},
44
+ }
45
+
46
+ with open(AUDIT_LOG_PATH, "a", encoding="utf-8") as f:
47
+ f.write(json.dumps(record) + "\n")
48
+
49
+ return AUDIT_LOG_PATH
50
+
51
+
52
+ def read_audit_log(last_n: int = 20) -> list[dict]:
53
+ """Read the last N scan records from the audit log."""
54
+ if not os.path.exists(AUDIT_LOG_PATH):
55
+ return []
56
+ records = []
57
+ try:
58
+ with open(AUDIT_LOG_PATH, encoding="utf-8") as f:
59
+ for line in f:
60
+ line = line.strip()
61
+ if line:
62
+ try:
63
+ records.append(json.loads(line))
64
+ except json.JSONDecodeError:
65
+ pass
66
+ except Exception:
67
+ pass
68
+ return records[-last_n:]
69
+
70
+
71
+ def export_audit_log_pdf(output_path: str = "depcheck-audit.json") -> str:
72
+ """Export full audit log as JSON for compliance teams."""
73
+ records = read_audit_log(last_n=9999)
74
+ with open(output_path, "w") as f:
75
+ json.dump({"generated_at": datetime.now(timezone.utc).isoformat(),
76
+ "total_scans": len(records),
77
+ "scans": records}, f, indent=2)
78
+ return output_path
@@ -0,0 +1,207 @@
1
+ import os
2
+ import json
3
+ import hashlib
4
+ import secrets
5
+ import time
6
+ from datetime import datetime, timezone
7
+
8
+ CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".depcheck")
9
+ AUTH_FILE = os.path.join(CONFIG_DIR, "auth.json")
10
+ KEYS_FILE = os.path.join(CONFIG_DIR, "api_keys.json")
11
+
12
+
13
+ def _ensure_dir():
14
+ os.makedirs(CONFIG_DIR, exist_ok=True)
15
+
16
+
17
+ def _load_auth() -> dict:
18
+ if not os.path.exists(AUTH_FILE):
19
+ return {}
20
+ with open(AUTH_FILE) as f:
21
+ return json.load(f)
22
+
23
+
24
+ def _save_auth(data: dict):
25
+ _ensure_dir()
26
+ with open(AUTH_FILE, "w") as f:
27
+ json.dump(data, f, indent=2)
28
+
29
+
30
+ def _load_keys() -> list[dict]:
31
+ if not os.path.exists(KEYS_FILE):
32
+ return []
33
+ with open(KEYS_FILE) as f:
34
+ return json.load(f)
35
+
36
+
37
+ def _save_keys(keys: list[dict]):
38
+ _ensure_dir()
39
+ with open(KEYS_FILE, "w") as f:
40
+ json.dump(keys, f, indent=2)
41
+
42
+
43
+ def generate_api_key(name: str, scopes: list = None) -> dict:
44
+ raw_key = "dc_" + secrets.token_urlsafe(32)
45
+ key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
46
+
47
+ entry = {
48
+ "id": secrets.token_hex(8),
49
+ "name": name,
50
+ "key_hash": key_hash,
51
+ "key_prefix": raw_key[:8],
52
+ "scopes": scopes or ["scan:read", "scan:write"],
53
+ "created_at": datetime.now(timezone.utc).isoformat(),
54
+ "last_used": None,
55
+ "active": True,
56
+ }
57
+
58
+ keys = _load_keys()
59
+ keys.append(entry)
60
+ _save_keys(keys)
61
+
62
+ result = dict(entry)
63
+ result["raw_key"] = raw_key
64
+ return result
65
+
66
+
67
+ def validate_api_key(raw_key: str) -> dict | None:
68
+ if not raw_key.startswith("dc_"):
69
+ return None
70
+ key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
71
+ keys = _load_keys()
72
+ for key in keys:
73
+ if key.get("key_hash") == key_hash and key.get("active"):
74
+ key["last_used"] = datetime.now(timezone.utc).isoformat()
75
+ _save_keys(keys)
76
+ return key
77
+ return None
78
+
79
+
80
+ def revoke_api_key(key_id: str) -> bool:
81
+ keys = _load_keys()
82
+ for key in keys:
83
+ if key["id"] == key_id:
84
+ key["active"] = False
85
+ _save_keys(keys)
86
+ return True
87
+ return False
88
+
89
+
90
+ def list_api_keys() -> list[dict]:
91
+ keys = _load_keys()
92
+ return [
93
+ {k: v for k, v in key.items() if k != "key_hash"}
94
+ for key in keys
95
+ ]
96
+
97
+
98
+ def login_oidc(provider="okta", client_id=None,
99
+ issuer_url=None) -> dict:
100
+ import webbrowser
101
+
102
+ client_id = client_id or os.environ.get(
103
+ "DEPCHECK_OIDC_CLIENT_ID", ""
104
+ )
105
+ issuer_url = issuer_url or os.environ.get(
106
+ "DEPCHECK_OIDC_ISSUER", ""
107
+ )
108
+
109
+ if not client_id or not issuer_url:
110
+ raise ValueError(
111
+ "Set DEPCHECK_OIDC_CLIENT_ID and DEPCHECK_OIDC_ISSUER.\n"
112
+ "Example:\n"
113
+ " export DEPCHECK_OIDC_CLIENT_ID=your-client-id\n"
114
+ " export DEPCHECK_OIDC_ISSUER=https://yourorg.okta.com"
115
+ "/oauth2/default"
116
+ )
117
+
118
+ import requests as req
119
+
120
+ device_url = f"{issuer_url}/v1/device/authorize"
121
+ token_url = f"{issuer_url}/v1/token"
122
+
123
+ resp = req.post(device_url, data={
124
+ "client_id": client_id,
125
+ "scope": "openid profile email",
126
+ }, timeout=10)
127
+
128
+ if resp.status_code != 200:
129
+ raise RuntimeError(f"Device auth failed: {resp.text[:200]}")
130
+
131
+ data = resp.json()
132
+ device_code = data["device_code"]
133
+ user_code = data["user_code"]
134
+ verification_uri = data["verification_uri"]
135
+ interval = data.get("interval", 5)
136
+ expires_in = data.get("expires_in", 300)
137
+
138
+ print(f"\n Open: {verification_uri}")
139
+ print(f" Code: {user_code}\n")
140
+ webbrowser.open(verification_uri)
141
+
142
+ deadline = time.time() + expires_in
143
+ while time.time() < deadline:
144
+ time.sleep(interval)
145
+ token_resp = req.post(token_url, data={
146
+ "client_id": client_id,
147
+ "device_code": device_code,
148
+ "grant_type":
149
+ "urn:ietf:params:oauth:grant-type:device_code",
150
+ }, timeout=10)
151
+
152
+ token_data = token_resp.json()
153
+ if "access_token" in token_data:
154
+ auth = {
155
+ "provider": provider,
156
+ "access_token": token_data["access_token"],
157
+ "id_token": token_data.get("id_token"),
158
+ "expires_at": (
159
+ datetime.now(timezone.utc).timestamp()
160
+ + token_data.get("expires_in", 3600)
161
+ ),
162
+ "logged_in_at": datetime.now(timezone.utc).isoformat(),
163
+ }
164
+ _save_auth(auth)
165
+ return auth
166
+
167
+ error = token_data.get("error")
168
+ if error not in ("authorization_pending", "slow_down"):
169
+ raise RuntimeError(f"Auth error: {error}")
170
+
171
+ raise TimeoutError("Login timed out.")
172
+
173
+
174
+ def get_current_auth() -> dict | None:
175
+ auth = _load_auth()
176
+ if not auth:
177
+ return None
178
+ if time.time() > auth.get("expires_at", 0):
179
+ return None
180
+ return auth
181
+
182
+
183
+ def logout() -> bool:
184
+ if os.path.exists(AUTH_FILE):
185
+ os.remove(AUTH_FILE)
186
+ return True
187
+ return False
188
+
189
+
190
+ def require_auth(console=None) -> dict | None:
191
+ api_key = os.environ.get("DEPCHECK_API_KEY")
192
+ if api_key:
193
+ key_entry = validate_api_key(api_key)
194
+ if key_entry:
195
+ return {"type": "api_key", "name": key_entry["name"],
196
+ "scopes": key_entry["scopes"]}
197
+ if console:
198
+ console.print(
199
+ "[yellow]Warning: DEPCHECK_API_KEY invalid.[/yellow]"
200
+ )
201
+ return None
202
+
203
+ auth = get_current_auth()
204
+ if auth:
205
+ return {"type": "oidc", "provider": auth.get("provider"),
206
+ "logged_in_at": auth.get("logged_in_at")}
207
+ return None
@@ -0,0 +1,57 @@
1
+ import json
2
+ import hashlib
3
+ import os
4
+ from datetime import datetime, timezone, timedelta
5
+
6
+ CACHE_DIR = os.path.join(os.path.expanduser("~"), ".depcheck", "cache")
7
+ CACHE_TTL_HOURS = 24
8
+
9
+
10
+ def _cache_path(key: str) -> str:
11
+ os.makedirs(CACHE_DIR, exist_ok=True)
12
+ hashed = hashlib.sha256(key.encode()).hexdigest()[:16]
13
+ return os.path.join(CACHE_DIR, f"{hashed}.json")
14
+
15
+
16
+ def cache_get(key: str) -> dict | None:
17
+ path = _cache_path(key)
18
+ if not os.path.exists(path):
19
+ return None
20
+ try:
21
+ with open(path) as f:
22
+ entry = json.load(f)
23
+ cached_at = datetime.fromisoformat(entry["cached_at"])
24
+ if datetime.now(timezone.utc) - cached_at > timedelta(hours=CACHE_TTL_HOURS):
25
+ os.remove(path)
26
+ return None
27
+ return entry["data"]
28
+ except Exception:
29
+ return None
30
+
31
+
32
+ def cache_set(key: str, data) -> None:
33
+ path = _cache_path(key)
34
+ try:
35
+ with open(path, "w") as f:
36
+ json.dump({"cached_at": datetime.now(timezone.utc).isoformat(), "data": data}, f)
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ def cache_clear() -> int:
42
+ if not os.path.exists(CACHE_DIR):
43
+ return 0
44
+ count = 0
45
+ for f in os.listdir(CACHE_DIR):
46
+ if f.endswith(".json"):
47
+ os.remove(os.path.join(CACHE_DIR, f))
48
+ count += 1
49
+ return count
50
+
51
+
52
+ def cache_stats() -> dict:
53
+ if not os.path.exists(CACHE_DIR):
54
+ return {"entries": 0, "size_kb": 0}
55
+ files = [f for f in os.listdir(CACHE_DIR) if f.endswith(".json")]
56
+ size = sum(os.path.getsize(os.path.join(CACHE_DIR, f)) for f in files)
57
+ return {"entries": len(files), "size_kb": round(size / 1024, 1)}
@@ -0,0 +1,79 @@
1
+ GITHUB_ACTIONS_TEMPLATE = """\
2
+ name: Dependency Security Scan
3
+
4
+ on:
5
+ push:
6
+ branches: [main, master]
7
+ pull_request:
8
+ branches: [main, master]
9
+ schedule:
10
+ - cron: '0 9 * * 1' # Every Monday 9am UTC
11
+
12
+ jobs:
13
+ depcheck:
14
+ name: oneport-depcheck
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: '3.11'
25
+
26
+ - name: Install dependencies
27
+ run: pip install -r requirements.txt
28
+
29
+ - name: Install oneport-depcheck
30
+ run: pip install oneport-depcheck
31
+
32
+ - name: Run vulnerability scan
33
+ run: |
34
+ depcheck scan -r requirements.txt --fail-on CRITICAL,HIGH
35
+
36
+ - name: Generate SBOM
37
+ run: depcheck sbom -r requirements.txt
38
+
39
+ - name: Upload SBOM as artifact
40
+ uses: actions/upload-artifact@v4
41
+ with:
42
+ name: sbom
43
+ path: |
44
+ sbom.spdx.json
45
+ sbom.cyclonedx.json
46
+ """
47
+
48
+ GITLAB_CI_TEMPLATE = """\
49
+ depcheck:
50
+ stage: test
51
+ image: python:3.11
52
+ script:
53
+ - pip install -r requirements.txt
54
+ - pip install oneport-depcheck
55
+ - depcheck scan -r requirements.txt --fail-on CRITICAL,HIGH
56
+ - depcheck sbom -r requirements.txt
57
+ artifacts:
58
+ paths:
59
+ - sbom.spdx.json
60
+ - sbom.cyclonedx.json
61
+ expire_in: 30 days
62
+ only:
63
+ - main
64
+ - merge_requests
65
+ """
66
+
67
+
68
+ def generate_github_actions(output_path: str = ".github/workflows/depcheck.yml") -> str:
69
+ import os
70
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
71
+ with open(output_path, "w") as f:
72
+ f.write(GITHUB_ACTIONS_TEMPLATE)
73
+ return output_path
74
+
75
+
76
+ def generate_gitlab_ci(output_path: str = "depcheck-gitlab.yml") -> str:
77
+ with open(output_path, "w") as f:
78
+ f.write(GITLAB_CI_TEMPLATE)
79
+ return output_path