devguard-scanner 0.1.0__tar.gz → 0.3.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.
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/.gitignore +2 -0
- devguard_scanner-0.3.0/CHANGELOG.md +59 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/PKG-INFO +38 -6
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/README.md +28 -1
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/docs/README.md +3 -1
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/pyproject.toml +7 -4
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/__init__.py +1 -1
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/registry.py +14 -2
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/run.py +7 -2
- devguard_scanner-0.3.0/src/devguard/analyzers/source.py +40 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/vscode.py +17 -10
- devguard_scanner-0.3.0/src/devguard/api/__init__.py +5 -0
- devguard_scanner-0.3.0/src/devguard/api/app.py +599 -0
- devguard_scanner-0.3.0/src/devguard/api/executor.py +95 -0
- devguard_scanner-0.3.0/src/devguard/api/limits.py +58 -0
- devguard_scanner-0.3.0/src/devguard/api/local.py +83 -0
- devguard_scanner-0.3.0/src/devguard/api/schemas.py +131 -0
- devguard_scanner-0.3.0/src/devguard/api/settings.py +136 -0
- devguard_scanner-0.3.0/src/devguard/api/traffic.py +151 -0
- devguard_scanner-0.3.0/src/devguard/api/worker.py +140 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/cli.py +155 -58
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/config.py +28 -9
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/models.py +21 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/service.py +19 -3
- devguard_scanner-0.3.0/src/devguard/remote/__init__.py +34 -0
- devguard_scanner-0.3.0/src/devguard/remote/archive.py +246 -0
- devguard_scanner-0.3.0/src/devguard/remote/errors.py +43 -0
- devguard_scanner-0.3.0/src/devguard/remote/fetch.py +202 -0
- devguard_scanner-0.3.0/src/devguard/remote/github.py +201 -0
- devguard_scanner-0.3.0/src/devguard/remote/url.py +128 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/markdown.py +9 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/sarif.py +21 -14
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/catalog.py +22 -0
- devguard_scanner-0.3.0/tests/github_fakes.py +135 -0
- devguard_scanner-0.3.0/tests/test_api.py +391 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_corpus.py +8 -0
- devguard_scanner-0.3.0/tests/test_remote.py +360 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_report_cli.py +14 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_safety.py +26 -2
- devguard_scanner-0.1.0/CHANGELOG.md +0 -23
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/LICENSE +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/investigator.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/prompts.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/anthropic.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/openai_compat.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/sdk.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/testing.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/tools.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/base.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/carriers.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/command.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/content.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/filesystem.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/hidden.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/imports.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/jsonc.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/ai_assistants.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/autoload.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/common.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/deps.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/devcontainer.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/env.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/git.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/npm.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/python_pkg.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/filetype.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/redact.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/safefs.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/scoring.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/correlate/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/correlate/chains.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/mcp/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/mcp/server.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/escape.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/terminal.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/writer.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/ai_instructions.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/cross_language.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/docs.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/javascript.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/obfuscation.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/python.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/shell.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/windows.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/data/vscode_settings.yaml +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/conftest.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/corpus/__init__.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/corpus/samples.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_ai.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_mcp.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_parsers.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_rules.py +0 -0
- {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_safefs.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/) and
|
|
4
|
+
the project uses [semantic versioning](https://semver.org).
|
|
5
|
+
|
|
6
|
+
## 0.3.0
|
|
7
|
+
|
|
8
|
+
- The default drop folder for local scans (`DEVGUARD_API_LOCAL_ROOT`, and the folder `compose.yaml` mounts) is
|
|
9
|
+
now `~/devguard-scanner-data` instead of `~/devguard-scanner`. Move the folder, or set the variable to keep
|
|
10
|
+
the old location.
|
|
11
|
+
- File locations use `devguard-scanner` (only the command is still `devguard`): config
|
|
12
|
+
`~/.config/devguard-scanner/config.toml` (the old `~/.config/devguard/config.toml` is still read if the new one
|
|
13
|
+
is missing), app data `~/.local/share/devguard-scanner/` (AI consent and cache, MCP audit log), and reports
|
|
14
|
+
`~/devguard-scanner/reports/` on every OS. In the container image the config is
|
|
15
|
+
`/etc/devguard-scanner/config.toml`.
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
Remote scanning, the HTTP API, and the container image.
|
|
20
|
+
|
|
21
|
+
- `devguard scan https://github.com/OWNER/REPO [--ref REF]`: fetches one commit as a tarball (no `git`), extracts
|
|
22
|
+
it with a hardened extractor, scans it, and deletes it. Private repositories work with your own token
|
|
23
|
+
(`DEVGUARD_GITHUB_TOKEN`, `GH_TOKEN`, or `GITHUB_TOKEN`).
|
|
24
|
+
- The fetched archive is reconciled with the git tree, and files hidden by `export-ignore` are fetched and
|
|
25
|
+
scanned. New rule `DG-GIT-EXPORT-IGNORE-001` (high when the hidden files are triggers or flagged code).
|
|
26
|
+
- Reports of remote scans carry a `source` section (repository, ref, commit). Markdown shows it, and SARIF uses
|
|
27
|
+
`versionControlProvenance`. The JSON field is optional, so existing consumers are unaffected.
|
|
28
|
+
- `--stdout md|json|sarif`; new exit code `5` (repository could not be fetched); `devguard doctor` shows whether
|
|
29
|
+
a GitHub token is set.
|
|
30
|
+
- HTTP API (`devguard-scanner[api]`, `devguard serve`): scans of public GitHub repositories with Swagger UI,
|
|
31
|
+
json/md/sarif output, a CI gate, per-scan worker processes with resource limits, rate limiting, and CDN
|
|
32
|
+
caching. Self-hosted instances can also scan local repositories linked (native) or bind-mounted read-only
|
|
33
|
+
(Docker) into a drop folder, without copying them.
|
|
34
|
+
- Optional AI investigation through the API with the caller's own Anthropic or OpenAI key. The key is used in
|
|
35
|
+
memory only, and the endpoint is fixed by the server.
|
|
36
|
+
- Optional MongoDB request log (`devguard-scanner[mongodb]`). It stores request metadata, the JSON report, and
|
|
37
|
+
the head of report.md, never the full report.md or AI keys, and expires records after 90 days by default.
|
|
38
|
+
- Container image `ghcr.io/yilmazhasan/devguard-scanner` (non-root, no git, multi-arch, signed, SBOM), plus
|
|
39
|
+
`compose.yaml` and the Vercel deployment in `deploy/vercel/`.
|
|
40
|
+
- Project URLs now point to `github.com/yilmazhasan/devguard-scanner`.
|
|
41
|
+
- VS Code task findings (`DG-VSC-TASK-AUTORUN-001`, `DG-VSC-TASK-HIDDEN-001`) now point at the `runOn` or
|
|
42
|
+
`presentation` line instead of the task's opening `{`, so the report excerpt shows the offending setting.
|
|
43
|
+
|
|
44
|
+
## 0.1.0
|
|
45
|
+
|
|
46
|
+
First release.
|
|
47
|
+
|
|
48
|
+
- Read-only scanner: no execution, no writes to the target, no symlink following, bounded reads and budgets.
|
|
49
|
+
- Surface analyzers for VS Code (tasks, launch, settings, workspaces, MCP), Claude Code hooks and permissions, MCP
|
|
50
|
+
configs (Cursor, Gemini, Windsurf, …), dev containers, npm/yarn/pnpm/bun, Python packaging, git config/hooks/
|
|
51
|
+
submodules, direnv and `.env` files, auto-loaded tool configs, and README instructions.
|
|
52
|
+
- 119 rules: content rules (YAML) for JavaScript/TypeScript, Python, shell, PowerShell/batch, AI instruction
|
|
53
|
+
files, and docs; hidden-content checks (bidi, invisible Unicode, off-screen code, encoded blobs); carriers
|
|
54
|
+
(executables, disguised files, archives, pickles); dependency sources and lockfiles.
|
|
55
|
+
- Attack-chain correlation from automatic triggers to dangerous code, up to three hops.
|
|
56
|
+
- Deterministic verdicts (`block` / `review` / `indicators` / `no-indicators`), Markdown, JSON, and SARIF reports.
|
|
57
|
+
- Optional AI investigation with pluggable providers (Claude, OpenAI, OpenAI-compatible, Python plugins), consent,
|
|
58
|
+
budgets, read-only tools, citation verification, and caching.
|
|
59
|
+
- MCP server with `scan_workspace`, `check_command`, findings, and rule tools.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devguard-scanner
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Scan untrusted code repositories for malware before you open, install, or run them. Files are analyzed as data and never executed.
|
|
5
|
-
Project-URL: Homepage, https://github.com/yilmazhasan/
|
|
6
|
-
Project-URL: Documentation, https://github.com/yilmazhasan/
|
|
7
|
-
Project-URL: Issues, https://github.com/yilmazhasan/
|
|
8
|
-
Project-URL: Changelog, https://github.com/yilmazhasan/
|
|
5
|
+
Project-URL: Homepage, https://github.com/yilmazhasan/devguard-scanner
|
|
6
|
+
Project-URL: Documentation, https://github.com/yilmazhasan/devguard-scanner/tree/main/docs
|
|
7
|
+
Project-URL: Issues, https://github.com/yilmazhasan/devguard-scanner/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yilmazhasan/devguard-scanner/blob/main/CHANGELOG.md
|
|
9
9
|
Author: yilmazhasan
|
|
10
10
|
License-Expression: Apache-2.0
|
|
11
11
|
License-File: LICENSE
|
|
@@ -25,8 +25,13 @@ Requires-Dist: pydantic<3,>=2.7
|
|
|
25
25
|
Requires-Dist: pyyaml<7,>=6.0.1
|
|
26
26
|
Provides-Extra: anthropic
|
|
27
27
|
Requires-Dist: anthropic<2,>=1.0; extra == 'anthropic'
|
|
28
|
+
Provides-Extra: api
|
|
29
|
+
Requires-Dist: fastapi<1,>=0.115; extra == 'api'
|
|
30
|
+
Requires-Dist: uvicorn<1,>=0.30; extra == 'api'
|
|
28
31
|
Provides-Extra: mcp
|
|
29
32
|
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
|
|
33
|
+
Provides-Extra: mongodb
|
|
34
|
+
Requires-Dist: pymongo<5,>=4.8; extra == 'mongodb'
|
|
30
35
|
Provides-Extra: openai
|
|
31
36
|
Requires-Dist: openai<4,>=1.50; extra == 'openai'
|
|
32
37
|
Description-Content-Type: text/markdown
|
|
@@ -60,7 +65,7 @@ Findings: 5 (1 critical, 2 high, 1 medium, 1 low)
|
|
|
60
65
|
- [critical] Automatic trigger reaches dangerous code (package.json:5)
|
|
61
66
|
- [high] Browser profile data targeted (lib/collect.js:7)
|
|
62
67
|
- [high] Cryptocurrency wallet data targeted (lib/collect.js:10)
|
|
63
|
-
Report (md):
|
|
68
|
+
Report (md): ~/devguard-scanner/reports/interview-task-1a2b3c4d/latest/report.md
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
| Verdict | Meaning |
|
|
@@ -72,12 +77,39 @@ Report (md): ~/.local/share/devguard/reports/interview-task-1a2b3c4d/latest/repo
|
|
|
72
77
|
|
|
73
78
|
Exit code `1` means findings at or above `--fail-on` (default `high`), so it works as a CI gate.
|
|
74
79
|
|
|
80
|
+
## Scan a GitHub repository without cloning it
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
devguard scan https://github.com/owner/repo # add --ref BRANCH|TAG|SHA
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The repository is downloaded into a temporary directory, scanned, and deleted. `git` is never run, so clone-time
|
|
87
|
+
exploits and hooks cannot trigger, and the files never sit in a folder your IDE could open. Private repositories
|
|
88
|
+
work with your own token (`GH_TOKEN`, `GITHUB_TOKEN`, or `DEVGUARD_GITHUB_TOKEN`).
|
|
89
|
+
|
|
90
|
+
## No install: hosted API or Docker
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# hosted API (public repositories), Swagger UI at https://devguard-scanner.vercel.app/docs
|
|
94
|
+
curl -s 'https://devguard-scanner.vercel.app/v1/scans/github/owner/repo?format=md'
|
|
95
|
+
|
|
96
|
+
# one-off scan in a throwaway container
|
|
97
|
+
docker run --rm ghcr.io/yilmazhasan/devguard-scanner scan https://github.com/owner/repo --stdout md
|
|
98
|
+
|
|
99
|
+
# self-hosted API, plus local scans of anything you copy into ~/devguard-scanner-data (mounted read-only)
|
|
100
|
+
docker compose up -d
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
See the [API guide](docs/api.md) and the [Docker guide](docs/docker.md). Optional AI explanations work with
|
|
104
|
+
your own provider key; the server never stores it.
|
|
105
|
+
|
|
75
106
|
## More
|
|
76
107
|
|
|
77
108
|
- [Getting started](docs/getting-started.md): options, reports, exit codes
|
|
78
109
|
- [AI investigation](docs/ai.md): optional explanations from Claude, OpenAI, Gemini, or a local model
|
|
79
110
|
- [Provider plugins](docs/plugins.md): connect any LLM with a small Python file
|
|
80
111
|
- [MCP server](docs/mcp.md): let Claude Code, Cursor, or Copilot check a repo before acting on it
|
|
112
|
+
- [HTTP API](docs/api.md) · [Docker](docs/docker.md): scan without installing, or self-host
|
|
81
113
|
- [CI and pre-commit](docs/ci.md) · [Configuration](docs/configuration.md) · [How it works](docs/how-it-works.md)
|
|
82
114
|
- [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md)
|
|
83
115
|
|
|
@@ -27,7 +27,7 @@ Findings: 5 (1 critical, 2 high, 1 medium, 1 low)
|
|
|
27
27
|
- [critical] Automatic trigger reaches dangerous code (package.json:5)
|
|
28
28
|
- [high] Browser profile data targeted (lib/collect.js:7)
|
|
29
29
|
- [high] Cryptocurrency wallet data targeted (lib/collect.js:10)
|
|
30
|
-
Report (md):
|
|
30
|
+
Report (md): ~/devguard-scanner/reports/interview-task-1a2b3c4d/latest/report.md
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
| Verdict | Meaning |
|
|
@@ -39,12 +39,39 @@ Report (md): ~/.local/share/devguard/reports/interview-task-1a2b3c4d/latest/repo
|
|
|
39
39
|
|
|
40
40
|
Exit code `1` means findings at or above `--fail-on` (default `high`), so it works as a CI gate.
|
|
41
41
|
|
|
42
|
+
## Scan a GitHub repository without cloning it
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
devguard scan https://github.com/owner/repo # add --ref BRANCH|TAG|SHA
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The repository is downloaded into a temporary directory, scanned, and deleted. `git` is never run, so clone-time
|
|
49
|
+
exploits and hooks cannot trigger, and the files never sit in a folder your IDE could open. Private repositories
|
|
50
|
+
work with your own token (`GH_TOKEN`, `GITHUB_TOKEN`, or `DEVGUARD_GITHUB_TOKEN`).
|
|
51
|
+
|
|
52
|
+
## No install: hosted API or Docker
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# hosted API (public repositories), Swagger UI at https://devguard-scanner.vercel.app/docs
|
|
56
|
+
curl -s 'https://devguard-scanner.vercel.app/v1/scans/github/owner/repo?format=md'
|
|
57
|
+
|
|
58
|
+
# one-off scan in a throwaway container
|
|
59
|
+
docker run --rm ghcr.io/yilmazhasan/devguard-scanner scan https://github.com/owner/repo --stdout md
|
|
60
|
+
|
|
61
|
+
# self-hosted API, plus local scans of anything you copy into ~/devguard-scanner-data (mounted read-only)
|
|
62
|
+
docker compose up -d
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See the [API guide](docs/api.md) and the [Docker guide](docs/docker.md). Optional AI explanations work with
|
|
66
|
+
your own provider key; the server never stores it.
|
|
67
|
+
|
|
42
68
|
## More
|
|
43
69
|
|
|
44
70
|
- [Getting started](docs/getting-started.md): options, reports, exit codes
|
|
45
71
|
- [AI investigation](docs/ai.md): optional explanations from Claude, OpenAI, Gemini, or a local model
|
|
46
72
|
- [Provider plugins](docs/plugins.md): connect any LLM with a small Python file
|
|
47
73
|
- [MCP server](docs/mcp.md): let Claude Code, Cursor, or Copilot check a repo before acting on it
|
|
74
|
+
- [HTTP API](docs/api.md) · [Docker](docs/docker.md): scan without installing, or self-host
|
|
48
75
|
- [CI and pre-commit](docs/ci.md) · [Configuration](docs/configuration.md) · [How it works](docs/how-it-works.md)
|
|
49
76
|
- [Contributing](CONTRIBUTING.md) · [Security policy](SECURITY.md)
|
|
50
77
|
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
| [Configuration](configuration.md) | The user config file, every option, limits, exclusions |
|
|
7
7
|
| [AI investigation](ai.md) | Enabling the optional AI layer, provider profiles (Claude, OpenAI, Gemini, local), consent, costs |
|
|
8
8
|
| [Provider plugins](plugins.md) | Writing your own LLM provider in Python, testing it, sharing it |
|
|
9
|
+
| [HTTP API](api.md) | The hosted and self-hosted API: endpoints, formats, limits, AI with your own key, request log |
|
|
10
|
+
| [Docker](docker.md) | One-off scans in a container, self-hosting, the local drop folder |
|
|
9
11
|
| [MCP server](mcp.md) | Using DevGuard from Claude Code, Cursor, VS Code Copilot, Gemini CLI |
|
|
10
12
|
| [CI and pre-commit](ci.md) | GitHub Actions, SARIF upload, pre-commit hook |
|
|
11
13
|
| [How it works](how-it-works.md) | Pipeline, triggers and attack chains, what is and is not detected |
|
|
12
14
|
| [Rules](rules.md) | Rule catalogue, writing a detection rule |
|
|
13
15
|
| [Threat reference](threats.md) | The `TC-xx` threat identifiers that rules reference |
|
|
14
16
|
| [Dependencies](dependencies.md) | Why each runtime dependency exists |
|
|
15
|
-
| [Releasing](releasing.md) | Publishing to PyPI (maintainers) |
|
|
17
|
+
| [Releasing](releasing.md) | Publishing to PyPI and GHCR, deploying the hosted API (maintainers) |
|
|
@@ -31,12 +31,14 @@ dependencies = [
|
|
|
31
31
|
anthropic = ["anthropic>=1.0,<2"]
|
|
32
32
|
openai = ["openai>=1.50,<4"]
|
|
33
33
|
mcp = ["mcp>=2.3,<3"]
|
|
34
|
+
api = ["fastapi>=0.115,<1", "uvicorn>=0.30,<1"]
|
|
35
|
+
mongodb = ["pymongo>=4.8,<5"]
|
|
34
36
|
|
|
35
37
|
[project.urls]
|
|
36
|
-
Homepage = "https://github.com/yilmazhasan/
|
|
37
|
-
Documentation = "https://github.com/yilmazhasan/
|
|
38
|
-
Issues = "https://github.com/yilmazhasan/
|
|
39
|
-
Changelog = "https://github.com/yilmazhasan/
|
|
38
|
+
Homepage = "https://github.com/yilmazhasan/devguard-scanner"
|
|
39
|
+
Documentation = "https://github.com/yilmazhasan/devguard-scanner/tree/main/docs"
|
|
40
|
+
Issues = "https://github.com/yilmazhasan/devguard-scanner/issues"
|
|
41
|
+
Changelog = "https://github.com/yilmazhasan/devguard-scanner/blob/main/CHANGELOG.md"
|
|
40
42
|
|
|
41
43
|
[project.scripts]
|
|
42
44
|
devguard = "devguard.cli:main"
|
|
@@ -52,6 +54,7 @@ dev = [
|
|
|
52
54
|
"ruff>=0.6",
|
|
53
55
|
"mypy>=1.11",
|
|
54
56
|
"types-PyYAML",
|
|
57
|
+
"httpx>=0.27", # FastAPI TestClient
|
|
55
58
|
]
|
|
56
59
|
|
|
57
60
|
[build-system]
|
|
@@ -93,7 +93,13 @@ def available_entry_points() -> dict[str, str]:
|
|
|
93
93
|
return out
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
def load_provider(
|
|
96
|
+
def load_provider(
|
|
97
|
+
cfg: AIConfig,
|
|
98
|
+
profile_name: str | None,
|
|
99
|
+
target: Path | None,
|
|
100
|
+
secrets: dict[str, str] | None = None,
|
|
101
|
+
) -> LoadedProvider:
|
|
102
|
+
"""Load a profile's provider. `secrets` supplies already-resolved values (an API caller's own key)."""
|
|
97
103
|
name = profile_name or cfg.default_profile
|
|
98
104
|
if not name:
|
|
99
105
|
raise ProviderError("no AI profile selected: pass --ai-profile or set ai.default_profile in your user config")
|
|
@@ -106,6 +112,8 @@ def load_provider(cfg: AIConfig, profile_name: str | None, target: Path | None)
|
|
|
106
112
|
settings["model"] = profile.model
|
|
107
113
|
for key, ref in profile.secrets.items():
|
|
108
114
|
settings[key] = resolve_secret(ref)
|
|
115
|
+
provided = dict(secrets or {})
|
|
116
|
+
settings.update(provided)
|
|
109
117
|
if profile.provider == "python":
|
|
110
118
|
factory, origin = _load_file_plugin(profile.plugin, target)
|
|
111
119
|
else:
|
|
@@ -118,7 +126,11 @@ def load_provider(cfg: AIConfig, profile_name: str | None, target: Path | None)
|
|
|
118
126
|
if not hasattr(provider, attr):
|
|
119
127
|
raise ProviderError(f"provider {profile.provider!r} does not implement AIProvider ({attr} missing)")
|
|
120
128
|
return LoadedProvider(
|
|
121
|
-
provider,
|
|
129
|
+
provider,
|
|
130
|
+
name,
|
|
131
|
+
profile,
|
|
132
|
+
origin,
|
|
133
|
+
{k: v for k, v in settings.items() if k not in profile.secrets and k not in provided},
|
|
122
134
|
)
|
|
123
135
|
|
|
124
136
|
|
|
@@ -51,15 +51,20 @@ def run_ai(
|
|
|
51
51
|
assume_consent: bool,
|
|
52
52
|
use_cache: bool = True,
|
|
53
53
|
ask: Callable[[str], bool] | None = None,
|
|
54
|
+
secrets: dict[str, str] | None = None,
|
|
54
55
|
) -> AIReport:
|
|
55
|
-
"""Run the AI stage. Never raises: every failure becomes AIReport(status="failed"/"skipped").
|
|
56
|
+
"""Run the AI stage. Never raises: every failure becomes AIReport(status="failed"/"skipped").
|
|
57
|
+
|
|
58
|
+
`secrets` passes already-resolved provider secrets (the API's per-request key) without touching the
|
|
59
|
+
environment, so concurrent requests never see each other's keys.
|
|
60
|
+
"""
|
|
56
61
|
try:
|
|
57
62
|
cfg = AIConfig.model_validate(ai_settings)
|
|
58
63
|
except Exception as exc:
|
|
59
64
|
return AIReport(status="failed", error=f"invalid [ai] configuration: {exc}")
|
|
60
65
|
target = Path(outcome.fs.root)
|
|
61
66
|
try:
|
|
62
|
-
loaded = load_provider(cfg, profile, target)
|
|
67
|
+
loaded = load_provider(cfg, profile, target, secrets)
|
|
63
68
|
except ProviderError as exc:
|
|
64
69
|
return AIReport(status="failed", profile=profile or cfg.default_profile, error=str(exc))
|
|
65
70
|
caps = loaded.provider.capabilities()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Checks on how a remote target was delivered (TC-06.8). Only runs for scans of fetched repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from devguard.analyzers.base import AnalysisContext
|
|
6
|
+
from devguard.core.models import Evidence, Finding, Severity, SourceInfo
|
|
7
|
+
from devguard.core.redact import visible
|
|
8
|
+
from devguard.rules.catalog import build_finding, rule
|
|
9
|
+
|
|
10
|
+
R_EXPORT_IGNORE = rule(
|
|
11
|
+
"DG-GIT-EXPORT-IGNORE-001",
|
|
12
|
+
"Files hidden from the repository's download archive",
|
|
13
|
+
"info",
|
|
14
|
+
"high",
|
|
15
|
+
"vcs",
|
|
16
|
+
"TC-06.8",
|
|
17
|
+
"Some files exist in the git tree but are left out of GitHub's download archive (`export-ignore` in "
|
|
18
|
+
".gitattributes). `git clone` still delivers them. DevGuard fetched them separately and scanned them. "
|
|
19
|
+
"When the hidden files contain automatic triggers or dangerous code, hiding them from archive-based "
|
|
20
|
+
"reviews looks deliberate.",
|
|
21
|
+
"Libraries (PHP packages in particular) routinely export-ignore tests, CI files, and docs to keep their "
|
|
22
|
+
"release archives small.",
|
|
23
|
+
"If the hidden files are triggers or flagged code, treat the repository as hostile: someone reviewing the "
|
|
24
|
+
"ZIP download would not have seen them.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def source_findings(source: SourceInfo | None, ctx: AnalysisContext, findings: list[Finding]) -> list[Finding]:
|
|
29
|
+
if source is None or not source.hidden_from_archive:
|
|
30
|
+
return []
|
|
31
|
+
hidden = set(source.hidden_from_archive)
|
|
32
|
+
trigger_paths = {t.path for t in ctx.triggers} | {r for t in ctx.triggers for r in t.references}
|
|
33
|
+
flagged = {f.primary_path for f in findings if f.severity.rank >= Severity.MEDIUM.rank}
|
|
34
|
+
suspicious = sorted(hidden & (trigger_paths | flagged))
|
|
35
|
+
shown = suspicious or sorted(hidden)
|
|
36
|
+
evidence = [Evidence(path=visible(p)) for p in shown[:20]]
|
|
37
|
+
detail = f"{len(hidden)} file(s) are missing from the archive."
|
|
38
|
+
if suspicious:
|
|
39
|
+
detail += f" {len(suspicious)} of them are automatic triggers, run by one, or flagged by other rules."
|
|
40
|
+
return [build_finding(R_EXPORT_IGNORE.id, evidence, severity=Severity.HIGH if suspicious else None, detail=detail)]
|
|
@@ -224,19 +224,23 @@ class VSCodeAnalyzer:
|
|
|
224
224
|
if is_auto:
|
|
225
225
|
shown = "; ".join(make_excerpt(c, limit=120) for _, c, _ in commands) or "(no command)"
|
|
226
226
|
direct = _run_on(t) == "folderOpen"
|
|
227
|
+
# Anchor on the line that makes the task auto-run, not the object's opening "{".
|
|
228
|
+
ro = t.get("runOptions")
|
|
229
|
+
anchor = (ro.get("runOn") if ro is not None else None) or label_node or t
|
|
227
230
|
out.append(
|
|
228
231
|
build_finding(
|
|
229
232
|
R_AUTORUN.id,
|
|
230
|
-
[fv.evidence_at_line(
|
|
233
|
+
[fv.evidence_at_line(anchor.line)],
|
|
231
234
|
detail=f"Task {make_excerpt(label, limit=80)!r}"
|
|
232
235
|
f"{'' if direct else ' (dependency of a folder-open task)'} runs: {shown}.",
|
|
233
236
|
)
|
|
234
237
|
)
|
|
235
|
-
|
|
238
|
+
hiding = _hiding_option(t)
|
|
239
|
+
if hiding is not None:
|
|
236
240
|
out.append(
|
|
237
241
|
build_finding(
|
|
238
242
|
R_HIDDEN_TASK.id,
|
|
239
|
-
[fv.evidence_at_line(
|
|
243
|
+
[fv.evidence_at_line(hiding.line)],
|
|
240
244
|
detail=f"Task {make_excerpt(label, limit=80)!r}.",
|
|
241
245
|
)
|
|
242
246
|
)
|
|
@@ -459,18 +463,21 @@ def _run_on(task: Node) -> str | None:
|
|
|
459
463
|
return run_on.value if run_on is not None and isinstance(run_on.value, str) else None
|
|
460
464
|
|
|
461
465
|
|
|
462
|
-
def
|
|
466
|
+
def _hiding_option(task: Node) -> Node | None:
|
|
467
|
+
"""The `presentation` option that hides the task's terminal output, if any."""
|
|
463
468
|
pres = task.get("presentation")
|
|
464
469
|
if pres is None:
|
|
465
|
-
return
|
|
470
|
+
return None
|
|
466
471
|
reveal = pres.get("reveal")
|
|
472
|
+
if reveal is not None and reveal.value == "never":
|
|
473
|
+
return reveal
|
|
467
474
|
echo = pres.get("echo")
|
|
475
|
+
if echo is not None and echo.value is False:
|
|
476
|
+
return echo
|
|
468
477
|
close = pres.get("close")
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
or (close is not None and close.value is True)
|
|
473
|
-
)
|
|
478
|
+
if close is not None and close.value is True:
|
|
479
|
+
return close
|
|
480
|
+
return None
|
|
474
481
|
|
|
475
482
|
|
|
476
483
|
def _task_commands(task: Node) -> list[tuple[str, str, int]]:
|