universal-dev-security 1.0.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.
Files changed (37) hide show
  1. universal_dev_security/__init__.py +3 -0
  2. universal_dev_security/__main__.py +4 -0
  3. universal_dev_security/cli.py +376 -0
  4. universal_dev_security/cli_extra.py +244 -0
  5. universal_dev_security/config.py +171 -0
  6. universal_dev_security/detector.py +201 -0
  7. universal_dev_security/engine.py +203 -0
  8. universal_dev_security/findings.py +199 -0
  9. universal_dev_security/fixes.py +138 -0
  10. universal_dev_security/gitguard.py +269 -0
  11. universal_dev_security/history.py +149 -0
  12. universal_dev_security/initializer.py +112 -0
  13. universal_dev_security/integrations/__init__.py +46 -0
  14. universal_dev_security/integrations/agents.py +45 -0
  15. universal_dev_security/integrations/claude_code.py +192 -0
  16. universal_dev_security/integrations/github.py +149 -0
  17. universal_dev_security/integrations/vscode.py +44 -0
  18. universal_dev_security/policy.py +384 -0
  19. universal_dev_security/projectfiles.py +80 -0
  20. universal_dev_security/recovery.py +168 -0
  21. universal_dev_security/redaction.py +132 -0
  22. universal_dev_security/reports.py +466 -0
  23. universal_dev_security/scanner.py +36 -0
  24. universal_dev_security/scanners/__init__.py +36 -0
  25. universal_dev_security/scanners/base.py +269 -0
  26. universal_dev_security/scanners/gitleaks.py +132 -0
  27. universal_dev_security/scanners/osv.py +156 -0
  28. universal_dev_security/scanners/semgrep.py +130 -0
  29. universal_dev_security/scanners/trivy.py +139 -0
  30. universal_dev_security/testing.py +100 -0
  31. universal_dev_security/updates.py +84 -0
  32. universal_dev_security-1.0.0.dist-info/METADATA +128 -0
  33. universal_dev_security-1.0.0.dist-info/RECORD +37 -0
  34. universal_dev_security-1.0.0.dist-info/WHEEL +5 -0
  35. universal_dev_security-1.0.0.dist-info/entry_points.txt +2 -0
  36. universal_dev_security-1.0.0.dist-info/licenses/LICENSE +163 -0
  37. universal_dev_security-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,130 @@
1
+ """Semgrep adapter: static code analysis (https://semgrep.dev)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from universal_dev_security.detector import has_source_code, recommended_semgrep_configs
10
+ from universal_dev_security.findings import Category, Finding, Severity
11
+ from universal_dev_security.projectfiles import ProjectInventory
12
+ from universal_dev_security.redaction import redact
13
+ from universal_dev_security.scanners.base import (
14
+ ScannerAdapter,
15
+ ScannerExecutionError,
16
+ ScanOptions,
17
+ relative_path,
18
+ run_tool,
19
+ )
20
+
21
+ # Semgrep rule severities. Current Semgrep rules may use CRITICAL/HIGH/MEDIUM/LOW
22
+ # directly; legacy values are documented by Semgrep as ERROR=High,
23
+ # WARNING=Medium, INFO=Low. Anything else stays unclassified.
24
+ SEVERITY_MAP = {
25
+ "CRITICAL": Severity.CRITICAL,
26
+ "HIGH": Severity.HIGH,
27
+ "ERROR": Severity.HIGH,
28
+ "MEDIUM": Severity.MEDIUM,
29
+ "WARNING": Severity.MEDIUM,
30
+ "LOW": Severity.LOW,
31
+ "INFO": Severity.LOW,
32
+ }
33
+
34
+
35
+ def _strings(value: Any, limit: int = 10) -> list[str]:
36
+ if isinstance(value, str):
37
+ return [value]
38
+ if isinstance(value, list):
39
+ return [str(v) for v in value if isinstance(v, (str, int, float))][:limit]
40
+ return []
41
+
42
+
43
+ def normalize(data: dict[str, Any], root: Path) -> list[Finding]:
44
+ results = data.get("results")
45
+ if not isinstance(results, list):
46
+ raise ScannerExecutionError("Semgrep output has an unexpected format.")
47
+ findings = []
48
+ for item in results:
49
+ if not isinstance(item, dict):
50
+ continue
51
+ extra = item.get("extra") if isinstance(item.get("extra"), dict) else {}
52
+ metadata = extra.get("metadata") if isinstance(extra.get("metadata"), dict) else {}
53
+ raw_severity = str(extra.get("severity") or "").upper()
54
+ severity = SEVERITY_MAP.get(raw_severity, Severity.UNCLASSIFIED)
55
+ start = item.get("start") if isinstance(item.get("start"), dict) else {}
56
+ end = item.get("end") if isinstance(item.get("end"), dict) else {}
57
+ rule_id = str(item.get("check_id") or "unknown")[:300]
58
+ cwe = ", ".join(_strings(metadata.get("cwe"), 3))
59
+ references = _strings(metadata.get("references")) + _strings(metadata.get("source"), 1)
60
+ message = redact(str(extra.get("message") or ""))[:800] # messages can interpolate matched code
61
+ findings.append(Finding(
62
+ tool="semgrep",
63
+ category=Category.CODE,
64
+ rule_id=rule_id,
65
+ title=rule_id.rsplit(".", 1)[-1].replace("-", " ").capitalize()[:200],
66
+ message=message,
67
+ file=relative_path(item.get("path"), root),
68
+ start_line=start.get("line"),
69
+ end_line=end.get("line"),
70
+ severity=severity,
71
+ severity_source="scanner" if severity is not Severity.UNCLASSIFIED else "none",
72
+ scanner_severity=raw_severity or None,
73
+ references=references,
74
+ remediation=("Review the flagged code and apply the rule guidance" + (f" ({cwe})." if cwe else ".")),
75
+ ))
76
+ return findings
77
+
78
+
79
+ class SemgrepAdapter(ScannerAdapter):
80
+ name = "semgrep"
81
+ display_name = "Semgrep"
82
+ category = Category.CODE
83
+ executable_names = ("semgrep",)
84
+ min_version = (1, 0, 0)
85
+ downloads_data = True
86
+ network_note = ("downloads rule packs from the Semgrep Registry when registry configs are used; "
87
+ "UDS disables Semgrep metrics and does not upload source code.")
88
+ docs_url = "https://semgrep.dev/docs/getting-started/quickstart"
89
+ install_hints = {
90
+ "windows": "python -m pip install semgrep (inside the UDS virtual environment)",
91
+ "macos": "brew install semgrep or python3 -m pip install semgrep",
92
+ "linux": "python3 -m pip install semgrep",
93
+ }
94
+
95
+ def applicable(self, inventory: ProjectInventory) -> tuple[bool, str]:
96
+ if has_source_code(inventory):
97
+ return True, "Source code files detected."
98
+ return False, "No source code files supported by Semgrep were found."
99
+
100
+ def configs(self, options: ScanOptions) -> list[str]:
101
+ configs = options.semgrep_configs or recommended_semgrep_configs(options.technologies)
102
+ # `--config auto` requires sending metrics to Semgrep; replace it with the default pack.
103
+ return [("p/default" if c.strip().lower() == "auto" else c.strip()) for c in configs if c.strip()][:20]
104
+
105
+ def build_command(self, executable: str, options: ScanOptions) -> list[str]:
106
+ command = [executable, "scan", "--json", "--metrics=off", "--disable-version-check", "--quiet",
107
+ "--timeout", "30"]
108
+ for config in self.configs(options):
109
+ command += ["--config", config]
110
+ for excluded in options.excluded_dirs:
111
+ if excluded != ".git":
112
+ command += ["--exclude", excluded]
113
+ return command + ["."]
114
+
115
+ def execute(self, executable: str, version: str, target: Path, options: ScanOptions) -> list[Finding]:
116
+ env = {"SEMGREP_SEND_METRICS": "off", "SEMGREP_ENABLE_VERSION_CHECK": "0"}
117
+ if os.name == "nt":
118
+ env["PYTHONUTF8"] = "1"
119
+ completed = run_tool(self.build_command(executable, options), cwd=target,
120
+ timeout=options.timeout_seconds, env_overrides=env)
121
+ # 0 = success, 1 = findings with --error. Everything else is a failure.
122
+ if completed.returncode not in (0, 1):
123
+ raise ScannerExecutionError(f"Semgrep exited with code {completed.returncode}; raw output was suppressed.")
124
+ try:
125
+ data = json.loads(completed.stdout or "")
126
+ except ValueError as exc:
127
+ raise ScannerExecutionError("Semgrep output could not be parsed; raw output was suppressed.") from exc
128
+ if not isinstance(data, dict):
129
+ raise ScannerExecutionError("Semgrep output has an unexpected format.")
130
+ return normalize(data, target)
@@ -0,0 +1,139 @@
1
+ """Trivy adapter: dependency vulnerabilities and misconfiguration (https://trivy.dev)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from universal_dev_security.detector import has_dependency_manifests, has_iac
10
+ from universal_dev_security.findings import Category, Finding, Severity
11
+ from universal_dev_security.projectfiles import ProjectInventory
12
+ from universal_dev_security.scanners.base import (
13
+ ScannerAdapter,
14
+ ScannerExecutionError,
15
+ ScanOptions,
16
+ relative_path,
17
+ run_tool,
18
+ )
19
+
20
+ # Trivy reports CRITICAL/HIGH/MEDIUM/LOW/UNKNOWN natively.
21
+ SEVERITY_MAP = {
22
+ "CRITICAL": Severity.CRITICAL,
23
+ "HIGH": Severity.HIGH,
24
+ "MEDIUM": Severity.MEDIUM,
25
+ "LOW": Severity.LOW,
26
+ }
27
+
28
+
29
+ def _refs(item: dict[str, Any]) -> list[str]:
30
+ refs = []
31
+ if item.get("PrimaryURL"):
32
+ refs.append(str(item["PrimaryURL"]))
33
+ refs.extend(str(r) for r in (item.get("References") or [])[:5] if isinstance(r, str))
34
+ return refs
35
+
36
+
37
+ def _severity(item: dict[str, Any]) -> tuple[Severity, str | None]:
38
+ raw = str(item.get("Severity") or "").upper()
39
+ return SEVERITY_MAP.get(raw, Severity.UNCLASSIFIED), raw or None
40
+
41
+
42
+ def normalize(data: dict[str, Any], root: Path) -> list[Finding]:
43
+ results = data.get("Results")
44
+ if results is None:
45
+ return []
46
+ if not isinstance(results, list):
47
+ raise ScannerExecutionError("Trivy output has an unexpected format.")
48
+ findings: list[Finding] = []
49
+ for result in results:
50
+ if not isinstance(result, dict):
51
+ continue
52
+ target_file = relative_path(result.get("Target"), root)
53
+ for vuln in result.get("Vulnerabilities") or []:
54
+ if not isinstance(vuln, dict):
55
+ continue
56
+ severity, raw = _severity(vuln)
57
+ pkg = str(vuln.get("PkgName") or "unknown")[:200]
58
+ fixed = str(vuln.get("FixedVersion") or "")[:200] or None
59
+ findings.append(Finding(
60
+ tool="trivy", category=Category.DEPENDENCY,
61
+ rule_id=str(vuln.get("VulnerabilityID") or "unknown")[:100],
62
+ title=str(vuln.get("Title") or f"Known vulnerability in {pkg}")[:300],
63
+ message=str(vuln.get("Description") or "")[:800],
64
+ file=target_file, package=pkg,
65
+ installed_version=str(vuln.get("InstalledVersion") or "")[:100] or None,
66
+ fixed_version=fixed, severity=severity,
67
+ severity_source="scanner" if severity is not Severity.UNCLASSIFIED else "none",
68
+ scanner_severity=raw, references=_refs(vuln),
69
+ remediation=(f"Upgrade {pkg} to {fixed} or later, then run the test suite."
70
+ if fixed else f"No fixed version is listed; assess exposure of {pkg}."),
71
+ ))
72
+ for misconf in result.get("Misconfigurations") or []:
73
+ if not isinstance(misconf, dict) or str(misconf.get("Status") or "FAIL").upper() == "PASS":
74
+ continue
75
+ severity, raw = _severity(misconf)
76
+ cause = misconf.get("CauseMetadata") if isinstance(misconf.get("CauseMetadata"), dict) else {}
77
+ findings.append(Finding(
78
+ tool="trivy", category=Category.CONFIGURATION,
79
+ rule_id=str(misconf.get("AVDID") or misconf.get("ID") or "unknown")[:100],
80
+ title=str(misconf.get("Title") or "Misconfiguration")[:300],
81
+ message=str(misconf.get("Message") or misconf.get("Description") or "")[:800],
82
+ file=target_file, start_line=cause.get("StartLine"), end_line=cause.get("EndLine"),
83
+ severity=severity,
84
+ severity_source="scanner" if severity is not Severity.UNCLASSIFIED else "none",
85
+ scanner_severity=raw, references=_refs(misconf),
86
+ remediation=str(misconf.get("Resolution") or "Apply the documented secure configuration.")[:500],
87
+ ))
88
+ return findings
89
+
90
+
91
+ class TrivyAdapter(ScannerAdapter):
92
+ name = "trivy"
93
+ display_name = "Trivy"
94
+ category = Category.CONFIGURATION
95
+ executable_names = ("trivy",)
96
+ min_version = (0, 45, 0)
97
+ downloads_data = True
98
+ network_note = ("downloads its vulnerability database and misconfiguration checks; "
99
+ "project files are analysed locally and are not uploaded.")
100
+ docs_url = "https://trivy.dev/latest/getting-started/installation/"
101
+ install_hints = {
102
+ "windows": "winget install --id AquaSecurity.Trivy -e",
103
+ "macos": "brew install trivy",
104
+ "linux": "Use the official repository or release from https://trivy.dev/latest/getting-started/installation/",
105
+ }
106
+
107
+ def applicable(self, inventory: ProjectInventory) -> tuple[bool, str]:
108
+ reasons = []
109
+ if has_dependency_manifests(inventory):
110
+ reasons.append("dependency manifests")
111
+ if has_iac(inventory):
112
+ reasons.append("container / infrastructure-as-code files")
113
+ if reasons:
114
+ return True, "Detected " + " and ".join(reasons) + "."
115
+ return False, "No dependency manifests or infrastructure-as-code files were found."
116
+
117
+ def build_command(self, executable: str, report: Path, options: ScanOptions) -> list[str]:
118
+ # Secret scanning is handled by Gitleaks; Trivy's secret output includes matched text.
119
+ command = [executable, "fs", "--quiet", "--format", "json", "--output", str(report),
120
+ "--scanners", "vuln,misconfig", "--exit-code", "0"]
121
+ skip = [d for d in options.excluded_dirs if d != ".git"]
122
+ if skip:
123
+ command += ["--skip-dirs", ",".join(skip)]
124
+ return command + ["."]
125
+
126
+ def execute(self, executable: str, version: str, target: Path, options: ScanOptions) -> list[Finding]:
127
+ with tempfile.TemporaryDirectory(prefix="uds-trivy-") as temp_dir:
128
+ report = Path(temp_dir) / "trivy.json"
129
+ completed = run_tool(self.build_command(executable, report, options), cwd=target,
130
+ timeout=options.timeout_seconds)
131
+ if completed.returncode != 0:
132
+ raise ScannerExecutionError(f"Trivy exited with code {completed.returncode}; raw output was suppressed.")
133
+ try:
134
+ data = json.loads(report.read_text(encoding="utf-8") or "{}")
135
+ except (OSError, ValueError, UnicodeDecodeError) as exc:
136
+ raise ScannerExecutionError("Trivy report could not be parsed; raw output was suppressed.") from exc
137
+ if not isinstance(data, dict):
138
+ raise ScannerExecutionError("Trivy output has an unexpected format.")
139
+ return normalize(data, target)
@@ -0,0 +1,100 @@
1
+ """Project test detection and execution (used by `uds test` and after fixes)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import tomllib
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from universal_dev_security.config import ProjectConfig
15
+ from universal_dev_security.redaction import redact
16
+
17
+ NPM_DEFAULT_TEST = 'echo "Error: no test specified" && exit 1'
18
+ OUTPUT_TAIL = 6000
19
+
20
+
21
+ def _project_python(root: Path) -> str:
22
+ for candidate in (root / ".venv" / "Scripts" / "python.exe", root / ".venv" / "bin" / "python",
23
+ root / "venv" / "Scripts" / "python.exe", root / "venv" / "bin" / "python"):
24
+ if candidate.is_file():
25
+ return str(candidate)
26
+ return sys.executable
27
+
28
+
29
+ def _uses_pytest(root: Path) -> bool:
30
+ if (root / "pytest.ini").is_file() or (root / "conftest.py").is_file():
31
+ return True
32
+ pyproject = root / "pyproject.toml"
33
+ if pyproject.is_file():
34
+ try:
35
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
36
+ except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError):
37
+ data = {}
38
+ if "pytest" in data.get("tool", {}):
39
+ return True
40
+ tests = root / "tests"
41
+ return tests.is_dir() and any(tests.glob("test_*.py"))
42
+
43
+
44
+ def detect_test_command(root: Path, config: ProjectConfig | None = None) -> tuple[list[str] | None, str]:
45
+ """Return (command, explanation). Commands are argument lists; no shell is used."""
46
+ root = root.expanduser().resolve()
47
+ if config and config.test_command:
48
+ return list(config.test_command), "configured in .uds/config.toml"
49
+ if _uses_pytest(root):
50
+ return [_project_python(root), "-m", "pytest", "-q"], "pytest project detected"
51
+ package = root / "package.json"
52
+ if package.is_file():
53
+ try:
54
+ scripts = json.loads(package.read_text(encoding="utf-8")).get("scripts", {})
55
+ except (OSError, ValueError, AttributeError):
56
+ scripts = {}
57
+ if isinstance(scripts, dict) and scripts.get("test") and scripts["test"].strip() != NPM_DEFAULT_TEST:
58
+ return ["npm", "test", "--silent"], "package.json test script"
59
+ if any(root.glob("*.sln")) or any(root.glob("*.csproj")):
60
+ return ["dotnet", "test"], ".NET solution/project"
61
+ if (root / "pom.xml").is_file():
62
+ return ["mvn", "-q", "test"], "Maven project"
63
+ if (root / "gradlew").is_file() or (root / "gradlew.bat").is_file():
64
+ return [str(root / ("gradlew.bat" if os.name == "nt" else "gradlew")), "test"], "Gradle wrapper"
65
+ if (root / "go.mod").is_file():
66
+ return ["go", "test", "./..."], "Go module"
67
+ if (root / "Cargo.toml").is_file():
68
+ return ["cargo", "test"], "Cargo project"
69
+ if (root / "vendor" / "bin" / "phpunit").is_file():
70
+ return [str(root / "vendor" / "bin" / "phpunit")], "PHPUnit"
71
+ return None, "No test suite detected. Configure [testing] command in .uds/config.toml."
72
+
73
+
74
+ def run_tests(root: Path, config: ProjectConfig | None = None, timeout: int | None = None) -> dict[str, Any]:
75
+ root = root.expanduser().resolve()
76
+ command, why = detect_test_command(root, config)
77
+ if not command:
78
+ return {"status": "not_configured", "passed": None, "command": None, "message": why}
79
+ executable = command[0]
80
+ resolved = executable if Path(executable).is_file() else shutil.which(executable)
81
+ if not resolved:
82
+ return {"status": "error", "passed": False, "command": command, "message": f"{executable!r} was not found on PATH."}
83
+ started = time.monotonic()
84
+ try:
85
+ # The command is the project's own, fixed test runner (no user-supplied arguments are
86
+ # interpolated), so running Windows .cmd shims such as npm.cmd is acceptable here.
87
+ completed = subprocess.run( # noqa: S603
88
+ [resolved, *command[1:]], cwd=str(root), stdin=subprocess.DEVNULL, capture_output=True, text=True,
89
+ encoding="utf-8", errors="replace", timeout=timeout or (config.test_timeout_seconds if config else 900),
90
+ check=False, shell=False,
91
+ )
92
+ except subprocess.TimeoutExpired:
93
+ return {"status": "timeout", "passed": False, "command": command, "message": "Test run timed out."}
94
+ except OSError as exc:
95
+ return {"status": "error", "passed": False, "command": command, "message": f"Could not start tests: {type(exc).__name__}"}
96
+ output = redact((completed.stdout or "") + (completed.stderr or ""))[-OUTPUT_TAIL:]
97
+ passed = completed.returncode == 0
98
+ return {"status": "passed" if passed else "failed", "passed": passed, "command": command, "reason": why,
99
+ "exit_code": completed.returncode, "duration_seconds": round(time.monotonic() - started, 2),
100
+ "output_tail": output}
@@ -0,0 +1,84 @@
1
+ """Update checks for UDS and its scanners.
2
+
3
+ UDS never updates itself or installs scanners. ``check_uds_update`` contacts a
4
+ network source only when one is given explicitly, sends nothing but an HTTPS
5
+ GET request, and only reports what is available.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import re
11
+ import urllib.error
12
+ import urllib.request
13
+ from typing import Any
14
+
15
+ from universal_dev_security import __version__
16
+ from universal_dev_security.scanners import available_scanners, get_scanner
17
+ from universal_dev_security.scanners.base import parse_version
18
+
19
+ PYPI_PROJECT = "universal-dev-security"
20
+ _REPO_RE = re.compile(r"^[A-Za-z0-9-]{1,39}/[A-Za-z0-9._-]{1,100}$")
21
+ MAX_RESPONSE = 2 * 1024 * 1024
22
+
23
+
24
+ class _HttpsOnlyRedirects(urllib.request.HTTPRedirectHandler):
25
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
26
+ if not str(newurl).startswith("https://"):
27
+ raise urllib.error.URLError("Refusing a redirect to a non-HTTPS URL")
28
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
29
+
30
+
31
+ def _fetch_json(url: str, timeout: int = 15) -> Any:
32
+ if not url.startswith("https://"):
33
+ raise ValueError("Only HTTPS update sources are allowed")
34
+ request = urllib.request.Request(url, headers={"User-Agent": f"uds-update-check/{__version__}",
35
+ "Accept": "application/json"})
36
+ opener = urllib.request.build_opener(_HttpsOnlyRedirects())
37
+ with opener.open(request, timeout=timeout) as response: # https enforced, including redirects
38
+ return json.loads(response.read(MAX_RESPONSE).decode("utf-8"))
39
+
40
+
41
+ def _version_tuple(text: str) -> tuple[int, ...]:
42
+ """Release-ordering key. Pre-releases (rc/a/b) sort before the final release."""
43
+ parsed = parse_version(text) or (0, 0, 0)
44
+ pre = 0 if re.search(r"\d[.-]?(?:rc|a|b|alpha|beta|dev)\d*$", text.strip().lower()) else 1
45
+ return (*parsed, pre)
46
+
47
+
48
+ def check_uds_update(source: str | None, fetch=_fetch_json) -> dict[str, Any]:
49
+ if not source:
50
+ return {"status": "source_not_configured", "current": __version__,
51
+ "message": "No update source given. Use --source pypi or --source github:OWNER/REPO. "
52
+ "UDS does not contact the network unless you ask it to."}
53
+ try:
54
+ if source == "pypi":
55
+ data = fetch(f"https://pypi.org/pypi/{PYPI_PROJECT}/json")
56
+ latest = str(data["info"]["version"])
57
+ url = f"https://pypi.org/project/{PYPI_PROJECT}/"
58
+ elif source.startswith("github:"):
59
+ repo = source.split(":", 1)[1]
60
+ if not _REPO_RE.match(repo):
61
+ raise ValueError("Invalid GitHub repository; expected OWNER/REPO")
62
+ data = fetch(f"https://api.github.com/repos/{repo}/releases/latest")
63
+ latest = str(data["tag_name"]).lstrip("v")
64
+ url = str(data.get("html_url") or f"https://github.com/{repo}/releases")
65
+ else:
66
+ raise ValueError("Unknown source; use 'pypi' or 'github:OWNER/REPO'")
67
+ except (urllib.error.URLError, TimeoutError, OSError) as exc:
68
+ return {"status": "error", "current": __version__, "message": f"Update check failed: {type(exc).__name__}"}
69
+ except (KeyError, TypeError, ValueError) as exc:
70
+ return {"status": "error", "current": __version__, "message": f"Update check failed: {exc}"}
71
+ newer = _version_tuple(latest) > _version_tuple(__version__)
72
+ return {"status": "update_available" if newer else "up_to_date", "current": __version__, "latest": latest,
73
+ "url": url, "message": ("A newer version is available. Review the changelog, then upgrade with "
74
+ "`python -m pip install --upgrade <verified package>` and run `uds doctor`.")
75
+ if newer else "UDS is up to date."}
76
+
77
+
78
+ def scanner_status() -> list[dict[str, Any]]:
79
+ rows = []
80
+ for name in available_scanners():
81
+ info = get_scanner(name).info()
82
+ rows.append({k: info[k] for k in ("name", "installed", "version", "minimum_version", "compatible",
83
+ "docs_url", "install_hints")})
84
+ return rows
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: universal-dev-security
3
+ Version: 1.0.0
4
+ Summary: One security system for every project: secrets, code, dependency and configuration scanning with a central policy engine, Git and AI-agent guards, and a local security dashboard.
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/jet1x/Universal-Dev-Security
7
+ Project-URL: Repository, https://github.com/jet1x/Universal-Dev-Security.git
8
+ Project-URL: Issues, https://github.com/jet1x/Universal-Dev-Security/issues
9
+ Project-URL: Changelog, https://github.com/jet1x/Universal-Dev-Security/blob/main/CHANGELOG.md
10
+ Project-URL: Security, https://github.com/jet1x/Universal-Dev-Security/blob/main/SECURITY.md
11
+ Keywords: security,devsecops,gitleaks,semgrep,osv-scanner,trivy,sast,sca,claude-code,secrets
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: Software Development :: Quality Assurance
26
+ Requires-Python: >=3.11
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # Universal Dev Security (UDS)
34
+
35
+ **One Security System. Every Project.**
36
+
37
+ UDS is an open-source, local-first security toolkit for developers. It runs
38
+ industry-standard scanners on any project, normalizes their results into one
39
+ schema, enforces a central security policy for Git and AI coding agents, and
40
+ produces a dark, self-contained HTML security dashboard.
41
+
42
+ > **Status:** `1.0.0` — first stable release. See
43
+ > [DEVELOPMENT_STATUS.md](DEVELOPMENT_STATUS.md) for verified functionality,
44
+ > known issues and test results. UDS reports what its scanners find; a clean
45
+ > result is never proof that a project is secure.
46
+
47
+ ## What it does
48
+
49
+ | Area | Capability |
50
+ |---|---|
51
+ | Secrets | [Gitleaks](https://github.com/gitleaks/gitleaks) directory, staged and outgoing-commit scanning (values always redacted) |
52
+ | Code | [Semgrep](https://semgrep.dev) static analysis with technology-based rule packs (metrics disabled) |
53
+ | Dependencies | [OSV-Scanner](https://google.github.io/osv-scanner/) and [Trivy](https://trivy.dev) vulnerability scanning |
54
+ | Configuration | Trivy misconfiguration scanning (Dockerfile, Kubernetes, Terraform, …) |
55
+ | Policy | Central engine: approval for `git push`, denial of hook bypasses and secret writes, project rules can only add restrictions |
56
+ | Git | `pre-commit` / `pre-push` hooks that block secrets (fail closed) |
57
+ | AI agents | Claude Code `PreToolUse` hooks; generic `uds guard` JSON protocol for any agent |
58
+ | GitHub | Generated PR workflow with checksum-verified scanners and SARIF upload |
59
+ | Reports | JSON, SARIF 2.1.0 and an offline dark HTML dashboard with filters, history and scan comparison |
60
+ | Recovery | Checkpoints before every UDS change; rollback that never clobbers later user edits |
61
+ | Fixes | Low-risk, reversible fixes verified by your test suite, rolled back automatically if tests fail |
62
+
63
+ Scanner states are explicit — `clean`, `findings`, `not_installed`,
64
+ `not_applicable`, `skipped`, `error` — and the overall status becomes
65
+ `incomplete` when an applicable scanner did not run. Severity is shown only
66
+ when the scanner supplies it or a published scale (CVSS) defines it; everything
67
+ else is `unclassified`. No security score is invented.
68
+
69
+ ## Quick start
70
+
71
+ Requirements: Python 3.11+, Windows, Linux or macOS. Git is recommended.
72
+
73
+ ```powershell
74
+ # Windows (from a release folder containing the wheel and SHA256SUMS)
75
+ .\scripts\install.ps1 -Wheel .\dist\1.0.0\universal_dev_security-1.0.0-py3-none-any.whl
76
+ ```
77
+
78
+ ```bash
79
+ # Linux / macOS
80
+ sh scripts/install.sh --wheel dist/1.0.0/universal_dev_security-1.0.0-py3-none-any.whl
81
+ ```
82
+
83
+ Then, inside a project:
84
+
85
+ ```bash
86
+ uds doctor # runtime, config and scanner availability (+ official install sources)
87
+ uds init # create .uds/config.toml (never overwrites)
88
+ uds scan # run applicable scanners, write JSON + HTML reports, record history
89
+ uds dashboard --open # open the dark HTML dashboard
90
+ uds hooks install --apply # block secrets at commit and push time
91
+ uds integrate claude-code --apply # enforce the UDS policy inside Claude Code
92
+ ```
93
+
94
+ Every command that changes files is a **dry run by default** and shows a diff;
95
+ `--apply` creates a recovery checkpoint first. See [docs/installation.md](docs/installation.md)
96
+ for scanner installation and upgrades, and [docs/commands.md](docs/commands.md) for all commands.
97
+
98
+ ## Documentation
99
+
100
+ - [Installation and upgrades](docs/installation.md)
101
+ - [Commands](docs/commands.md)
102
+ - [Scanners, severity and privacy](docs/scanners.md)
103
+ - [Integrations: Claude Code, GitHub, VS Code, other agents](docs/integrations.md)
104
+ - [Architecture](docs/architecture.md)
105
+ - [Security model](docs/security-model.md) and [global policy](policies/GLOBAL_SECURITY_POLICY.md)
106
+ - [Dashboard design system](docs/dashboard-design-system.md)
107
+ - [Release process](docs/release.md)
108
+
109
+ ## Safety guarantees
110
+
111
+ - No telemetry. The core never contacts the network; scanners' network use is documented and
112
+ OSV-Scanner (which sends package names/versions to osv.dev) runs only with explicit approval.
113
+ - UDS never installs scanners, never commits, never pushes and never publishes.
114
+ - Existing configuration is merged, never silently overwritten; invalid files are left untouched.
115
+ - Secret values, matched text and raw scanner output never appear in output, reports or history.
116
+ - Global policies cannot be weakened by project configuration or AI tools.
117
+
118
+ Local Git hooks and AI-agent hooks are defense in depth — keep CI scanning and
119
+ repository-level protections (for example GitHub push protection) enabled too.
120
+
121
+ ## Contributing and security
122
+
123
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
124
+ Changes are listed in [CHANGELOG.md](CHANGELOG.md).
125
+
126
+ ## License
127
+
128
+ Apache License 2.0. See [LICENSE](LICENSE).
@@ -0,0 +1,37 @@
1
+ universal_dev_security/__init__.py,sha256=b5Kl-jI1dq6h4flmJzGOz-7yvG4HNBmMvsRdoJLlp3I,101
2
+ universal_dev_security/__main__.py,sha256=RQ8MzzL5ITUXhnOLEODb1QGzdvgXiiXcv05Dn5G7FSk,120
3
+ universal_dev_security/cli.py,sha256=xCYUtrLp8fH-de6WurD0h8V8gxlNyk2kz_byes_rrtc,17011
4
+ universal_dev_security/cli_extra.py,sha256=XpuMUkjNlS0UQNVCwIiVBBddCTh-3q6x6p6kvlz6mQY,10871
5
+ universal_dev_security/config.py,sha256=vqwKZKKR_we4oohjfZMHwgP0W4dtSYZoH0POCYwo8TU,7992
6
+ universal_dev_security/detector.py,sha256=KnBbTJFX2uthr0d8dl-wepckvTk7K-njNwBzZgIGpwo,10103
7
+ universal_dev_security/engine.py,sha256=Dz5UJjxmoZh_SEEjWkNjQcAeUjdpcoUrnWvta4UYWv4,8973
8
+ universal_dev_security/findings.py,sha256=8-RrA0U8p0j40ZCxOutlunoPLmCl2auayiDshH5zQfI,6736
9
+ universal_dev_security/fixes.py,sha256=ha1_NmteDt3kNOtg9CUwcuPY3gsTyajX6tG2ugzzpJA,6214
10
+ universal_dev_security/gitguard.py,sha256=2kuQBQ5rqpCqfNTs7KxBAxDKPToRmOSlUBOPYe6taDI,11263
11
+ universal_dev_security/history.py,sha256=QkPHIMY9dP-bcnuVscIhp2dvVj12Hm6QNgEYtQPIJT4,6084
12
+ universal_dev_security/initializer.py,sha256=npH_3USo2jhkfWMO65XDWPYZsW0-CKLVoj6-MEh_YsY,4259
13
+ universal_dev_security/policy.py,sha256=9ItX9ANR6vT4o5C8edorjz4TbsQhiQWco-4MqfRrDWE,20018
14
+ universal_dev_security/projectfiles.py,sha256=WYrg5coVY6y3ZaDTa0PxwGcIiS-Toal4kejhA3nUIYc,2983
15
+ universal_dev_security/recovery.py,sha256=C-O2_xNfZoTTrWFnA_7bDiWiXaETyCHuh3c7xlwPmAc,7234
16
+ universal_dev_security/redaction.py,sha256=TkwrwM7YQF2PGJ7N6PcWXzx2cQTg6hLA-ylz7NLJ6wg,5595
17
+ universal_dev_security/reports.py,sha256=-H38BMyaLTK9VY-wh5hjUKgeMtmB4gATt6MMqINZG-Y,31640
18
+ universal_dev_security/scanner.py,sha256=IbasoQc49eIjKgS1SSiNrgPcbWUXbNgpgGGM5Tf2UV0,1308
19
+ universal_dev_security/testing.py,sha256=dWeCEhgkqODzHmE0eCRBXWgA2xi24MP8mMkARaF0C_g,4804
20
+ universal_dev_security/updates.py,sha256=UqRWAl4Km8XNxnnXKxa2a-LH-cuqfQKc6KB5lRbBkUg,4190
21
+ universal_dev_security/integrations/__init__.py,sha256=cp0s3KYhIpk5w7bYmGaW8ZPp61aneBivGEDGbwZrcDY,2013
22
+ universal_dev_security/integrations/agents.py,sha256=bCom-QgbACFT-PEErxb1VCzQc2D5EvGlRB4mVnQoVTs,1869
23
+ universal_dev_security/integrations/claude_code.py,sha256=-0hPYrayogCTRdtYw1g71EudrpxViW_9iexS4f4_ipA,9369
24
+ universal_dev_security/integrations/github.py,sha256=3kHUqF5-smbSgwE4UyoeLSGAujUB09qFVJCbRNyN-IQ,6500
25
+ universal_dev_security/integrations/vscode.py,sha256=JAreeGLGglQg7J1MdpMpH--KGDji1t4W-92nA55Ycb8,2076
26
+ universal_dev_security/scanners/__init__.py,sha256=DYc2eNyhj3g-8zebajxc7sbOgVHzGfc_e8moJYlsDDI,1268
27
+ universal_dev_security/scanners/base.py,sha256=Zms8fwESZEOJueLDrm648TexcFQcUI1mBzEwyP9Z4Lo,11186
28
+ universal_dev_security/scanners/gitleaks.py,sha256=WYMiGy4hO3exQF5YbdE76vTTyVqgSPXPg-hz42Noynw,6211
29
+ universal_dev_security/scanners/osv.py,sha256=WbDzC4Z73PwmElNaeLs2hZxGDFnPMnP3gkHkjEazp78,7852
30
+ universal_dev_security/scanners/semgrep.py,sha256=PpSxUe8dz222EK3mCzUR8zBTbjL8QHJ_-Y2DTB4Ca_A,5959
31
+ universal_dev_security/scanners/trivy.py,sha256=ii4dMDiuw2v6taw90Gr1aOZb_T-iwMInIadwkWoiqg0,6685
32
+ universal_dev_security-1.0.0.dist-info/licenses/LICENSE,sha256=ReLrsC1BngysN-NSvsmNb5WaZ_1UOweZEZne3VkEzFY,9156
33
+ universal_dev_security-1.0.0.dist-info/METADATA,sha256=383DW1f9ULS0X1W0WGH_y5XGYowNbrIUgwu-_N43hjs,6531
34
+ universal_dev_security-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
35
+ universal_dev_security-1.0.0.dist-info/entry_points.txt,sha256=3KgyciEul9-L9tn_V8fAIf7R8qtJdbyg77ApiwO2iVs,56
36
+ universal_dev_security-1.0.0.dist-info/top_level.txt,sha256=FOgKdp-7KYU_0VL00cer496y6qMWuSSy3b8RjzFyv3Y,23
37
+ universal_dev_security-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ uds = universal_dev_security.cli:main