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.
Files changed (101) hide show
  1. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/.gitignore +2 -0
  2. devguard_scanner-0.3.0/CHANGELOG.md +59 -0
  3. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/PKG-INFO +38 -6
  4. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/README.md +28 -1
  5. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/docs/README.md +3 -1
  6. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/pyproject.toml +7 -4
  7. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/__init__.py +1 -1
  8. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/registry.py +14 -2
  9. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/run.py +7 -2
  10. devguard_scanner-0.3.0/src/devguard/analyzers/source.py +40 -0
  11. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/vscode.py +17 -10
  12. devguard_scanner-0.3.0/src/devguard/api/__init__.py +5 -0
  13. devguard_scanner-0.3.0/src/devguard/api/app.py +599 -0
  14. devguard_scanner-0.3.0/src/devguard/api/executor.py +95 -0
  15. devguard_scanner-0.3.0/src/devguard/api/limits.py +58 -0
  16. devguard_scanner-0.3.0/src/devguard/api/local.py +83 -0
  17. devguard_scanner-0.3.0/src/devguard/api/schemas.py +131 -0
  18. devguard_scanner-0.3.0/src/devguard/api/settings.py +136 -0
  19. devguard_scanner-0.3.0/src/devguard/api/traffic.py +151 -0
  20. devguard_scanner-0.3.0/src/devguard/api/worker.py +140 -0
  21. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/cli.py +155 -58
  22. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/config.py +28 -9
  23. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/models.py +21 -0
  24. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/service.py +19 -3
  25. devguard_scanner-0.3.0/src/devguard/remote/__init__.py +34 -0
  26. devguard_scanner-0.3.0/src/devguard/remote/archive.py +246 -0
  27. devguard_scanner-0.3.0/src/devguard/remote/errors.py +43 -0
  28. devguard_scanner-0.3.0/src/devguard/remote/fetch.py +202 -0
  29. devguard_scanner-0.3.0/src/devguard/remote/github.py +201 -0
  30. devguard_scanner-0.3.0/src/devguard/remote/url.py +128 -0
  31. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/markdown.py +9 -0
  32. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/sarif.py +21 -14
  33. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/catalog.py +22 -0
  34. devguard_scanner-0.3.0/tests/github_fakes.py +135 -0
  35. devguard_scanner-0.3.0/tests/test_api.py +391 -0
  36. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_corpus.py +8 -0
  37. devguard_scanner-0.3.0/tests/test_remote.py +360 -0
  38. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_report_cli.py +14 -0
  39. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_safety.py +26 -2
  40. devguard_scanner-0.1.0/CHANGELOG.md +0 -23
  41. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/LICENSE +0 -0
  42. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/__init__.py +0 -0
  43. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/investigator.py +0 -0
  44. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/prompts.py +0 -0
  45. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/__init__.py +0 -0
  46. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/anthropic.py +0 -0
  47. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/providers/openai_compat.py +0 -0
  48. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/sdk.py +0 -0
  49. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/testing.py +0 -0
  50. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/ai/tools.py +0 -0
  51. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/__init__.py +0 -0
  52. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/base.py +0 -0
  53. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/carriers.py +0 -0
  54. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/command.py +0 -0
  55. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/content.py +0 -0
  56. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/filesystem.py +0 -0
  57. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/hidden.py +0 -0
  58. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/imports.py +0 -0
  59. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/jsonc.py +0 -0
  60. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/__init__.py +0 -0
  61. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/ai_assistants.py +0 -0
  62. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/autoload.py +0 -0
  63. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/common.py +0 -0
  64. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/deps.py +0 -0
  65. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/devcontainer.py +0 -0
  66. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/env.py +0 -0
  67. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/git.py +0 -0
  68. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/npm.py +0 -0
  69. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/analyzers/surfaces/python_pkg.py +0 -0
  70. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/__init__.py +0 -0
  71. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/filetype.py +0 -0
  72. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/redact.py +0 -0
  73. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/safefs.py +0 -0
  74. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/core/scoring.py +0 -0
  75. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/correlate/__init__.py +0 -0
  76. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/correlate/chains.py +0 -0
  77. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/mcp/__init__.py +0 -0
  78. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/mcp/server.py +0 -0
  79. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/__init__.py +0 -0
  80. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/escape.py +0 -0
  81. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/terminal.py +0 -0
  82. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/report/writer.py +0 -0
  83. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/__init__.py +0 -0
  84. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/ai_instructions.yaml +0 -0
  85. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/cross_language.yaml +0 -0
  86. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/docs.yaml +0 -0
  87. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/javascript.yaml +0 -0
  88. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/obfuscation.yaml +0 -0
  89. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/python.yaml +0 -0
  90. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/shell.yaml +0 -0
  91. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/content/windows.yaml +0 -0
  92. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/src/devguard/rules/data/vscode_settings.yaml +0 -0
  93. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/__init__.py +0 -0
  94. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/conftest.py +0 -0
  95. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/corpus/__init__.py +0 -0
  96. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/corpus/samples.py +0 -0
  97. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_ai.py +0 -0
  98. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_mcp.py +0 -0
  99. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_parsers.py +0 -0
  100. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_rules.py +0 -0
  101. {devguard_scanner-0.1.0 → devguard_scanner-0.3.0}/tests/test_safefs.py +0 -0
@@ -1,6 +1,8 @@
1
1
  # Private planning material (kept out of the repository)
2
2
  docs/plan/
3
3
  docs/previous/
4
+ DESIGN.md
5
+ TASKS.md
4
6
 
5
7
  # Python
6
8
  .venv/
@@ -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.1.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/dev-guard
6
- Project-URL: Documentation, https://github.com/yilmazhasan/dev-guard/tree/main/docs
7
- Project-URL: Issues, https://github.com/yilmazhasan/dev-guard/issues
8
- Project-URL: Changelog, https://github.com/yilmazhasan/dev-guard/blob/main/CHANGELOG.md
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): ~/.local/share/devguard/reports/interview-task-1a2b3c4d/latest/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): ~/.local/share/devguard/reports/interview-task-1a2b3c4d/latest/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/dev-guard"
37
- Documentation = "https://github.com/yilmazhasan/dev-guard/tree/main/docs"
38
- Issues = "https://github.com/yilmazhasan/dev-guard/issues"
39
- Changelog = "https://github.com/yilmazhasan/dev-guard/blob/main/CHANGELOG.md"
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]
@@ -1,5 +1,5 @@
1
1
  """DevGuard: pre-open security scanner for untrusted code workspaces."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.0"
4
4
  RULESET_VERSION = "2026.10.0"
5
5
  SCHEMA_VERSION = "1"
@@ -93,7 +93,13 @@ def available_entry_points() -> dict[str, str]:
93
93
  return out
94
94
 
95
95
 
96
- def load_provider(cfg: AIConfig, profile_name: str | None, target: Path | None) -> LoadedProvider:
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, name, profile, origin, {k: v for k, v in settings.items() if k not in profile.secrets}
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(t.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
- if _hides_output(t):
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(t.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 _hides_output(task: Node) -> bool:
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 False
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
- return (
470
- (reveal is not None and reveal.value == "never")
471
- or (echo is not None and echo.value is False)
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]]:
@@ -0,0 +1,5 @@
1
+ """DevGuard HTTP API (extra `devguard-scanner[api]`). Start it with `devguard serve`.
2
+
3
+ This package must stay importable without FastAPI: the worker process (`devguard.api.worker`) only needs the
4
+ base install. FastAPI is imported by `devguard.api.app` alone.
5
+ """