@praneeth_54/agentdoctor 0.1.1-beta → 0.1.3-beta
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.
- package/CHANGELOG.md +48 -1
- package/README.md +5 -5
- package/dist/cli/index.js +0 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/rules/build-context.js +36 -5
- package/dist/core/rules/context/generated-directory.js +33 -43
- package/dist/core/rules/ignore.d.ts +5 -0
- package/dist/core/rules/ignore.js +51 -9
- package/dist/core/rules/security/env-file-exposure.d.ts +6 -0
- package/dist/core/rules/security/env-file-exposure.js +78 -9
- package/dist/core/rules/security/private-key-file.js +10 -4
- package/dist/core/scanner/scan.js +6 -0
- package/dist/detectors/framework.d.ts +3 -0
- package/dist/detectors/framework.js +19 -9
- package/dist/detectors/language.d.ts +1 -0
- package/dist/detectors/language.js +7 -0
- package/dist/detectors/monorepo.d.ts +2 -1
- package/dist/detectors/monorepo.js +32 -1
- package/dist/detectors/package-manager.d.ts +7 -1
- package/dist/detectors/package-manager.js +147 -22
- package/dist/detectors/project.js +77 -17
- package/dist/detectors/python-deps.d.ts +6 -0
- package/dist/detectors/python-deps.js +55 -0
- package/dist/index.d.ts +1 -1
- package/dist/reporters/json/report.js +1 -0
- package/dist/reporters/terminal/report.js +37 -7
- package/dist/types/index.d.ts +12 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
13
|
- Safe automatic fixes
|
|
14
14
|
- Packaged GitHub Action
|
|
15
15
|
|
|
16
|
+
## [0.1.3-beta] — 2026-07-29
|
|
17
|
+
|
|
18
|
+
Backward-compatible security-signal and detection-quality patch.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- Repositories without supported agent configuration no longer receive an unqualified clean result when relevant repository-risk findings exist
|
|
23
|
+
- Agent exposure and repository risk remain semantically distinct (`affectedAgents` is empty when exposure is not asserted)
|
|
24
|
+
- Additive `agentSecurityAnalysis` field: `full` | `limited`
|
|
25
|
+
- Environment templates (`.env.example`, `.env.sample`, `.env.template`, `.env.dist`) receive informational treatment
|
|
26
|
+
- Common environment backup filenames (`.env_backup`, `.env_old`, `.env_local`) receive conservative warning treatment
|
|
27
|
+
|
|
28
|
+
### Detection
|
|
29
|
+
|
|
30
|
+
- Prevent false Django detection from arbitrary nested `settings.py` files
|
|
31
|
+
- Detect nested FastAPI projects using dependency + `app/main.py` structure evidence
|
|
32
|
+
- Detect React from nested project manifests
|
|
33
|
+
- Prevent Poetry detection from generic non-Poetry `pyproject.toml` files (for example hatchling)
|
|
34
|
+
- Improve multi-stack terminal summaries for multi-project repositories
|
|
35
|
+
|
|
36
|
+
### Testing
|
|
37
|
+
|
|
38
|
+
- Added Excepta FastAPI/React/Flutter and security-semantics fixtures/tests
|
|
39
|
+
- Total test suite is now 122 tests
|
|
40
|
+
- Three real-world regression families validated (ProxyShield, Flutter/Laravel multi-app, Excepta)
|
|
41
|
+
|
|
42
|
+
## [0.1.2-beta] — 2026-07-29
|
|
43
|
+
|
|
44
|
+
Backward-compatible quality patch.
|
|
45
|
+
|
|
46
|
+
### Fixed
|
|
47
|
+
|
|
48
|
+
- More reliable CLI/package binary execution (local bin shim + executable CLI entry)
|
|
49
|
+
- Nested `context/generated-directory` ignore handling and precise evidence paths
|
|
50
|
+
- Multi-project repository detection without requiring Node workspace tooling
|
|
51
|
+
- Mixed Composer / Pub / npm package-manager detection
|
|
52
|
+
- False malformed-package diagnostic when root `package.json` is absent
|
|
53
|
+
- Conservative credential-file detection for `.der` and service-account JSON names
|
|
54
|
+
- Quieter permission diagnostics under already-skipped directories
|
|
55
|
+
|
|
56
|
+
### Testing
|
|
57
|
+
|
|
58
|
+
- Added real-world multi-app, generated-directory, credential, and CLI bin fixtures/tests
|
|
59
|
+
- Total test suite is now 104 tests
|
|
60
|
+
|
|
16
61
|
## [0.1.1-beta] — 2026-07-29
|
|
17
62
|
|
|
18
63
|
Patch release.
|
|
@@ -62,6 +107,8 @@ First public beta.
|
|
|
62
107
|
- Not a complete secret scanner
|
|
63
108
|
- Git “tracked secret” detection deferred
|
|
64
109
|
|
|
65
|
-
[Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.1.
|
|
110
|
+
[Unreleased]: https://github.com/pranee54/AgentDoctor/compare/v0.1.3-beta...HEAD
|
|
111
|
+
[0.1.3-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.3-beta
|
|
112
|
+
[0.1.2-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.2-beta
|
|
66
113
|
[0.1.1-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.1-beta
|
|
67
114
|
[0.1.0-beta]: https://github.com/pranee54/AgentDoctor/releases/tag/v0.1.0-beta
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/pranee54/AgentDoctor/stargazers)
|
|
6
6
|
[](https://github.com/pranee54/AgentDoctor/actions)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
|
-
[](CHANGELOG.md)
|
|
9
9
|
|
|
10
10
|
**Lighthouse for AI coding agents.**
|
|
11
11
|
|
|
@@ -29,7 +29,7 @@ npx @praneeth_54/agentdoctor
|
|
|
29
29
|
```text
|
|
30
30
|
$ npx @praneeth_54/agentdoctor
|
|
31
31
|
|
|
32
|
-
AgentDoctor v0.1.
|
|
32
|
+
AgentDoctor v0.1.3-beta
|
|
33
33
|
|
|
34
34
|
Repository
|
|
35
35
|
Framework: Next.js
|
|
@@ -88,13 +88,13 @@ AgentDoctor analyzes configuration files. It does not run agents or edit your pr
|
|
|
88
88
|
### One-shot
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
npx @praneeth_54/agentdoctor@0.1.
|
|
91
|
+
npx @praneeth_54/agentdoctor@0.1.3-beta
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
### Global (optional)
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
npm install -g @praneeth_54/agentdoctor@0.1.
|
|
97
|
+
npm install -g @praneeth_54/agentdoctor@0.1.3-beta
|
|
98
98
|
agentdoctor
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -225,7 +225,7 @@ npx @praneeth_54/agentdoctor --json
|
|
|
225
225
|
|
|
226
226
|
```json
|
|
227
227
|
{
|
|
228
|
-
"version": "0.1.
|
|
228
|
+
"version": "0.1.3-beta",
|
|
229
229
|
"repository": {
|
|
230
230
|
"primaryFramework": "nextjs",
|
|
231
231
|
"primaryLanguage": "typescript",
|
package/dist/cli/index.js
CHANGED
|
File without changes
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "0.1.
|
|
1
|
+
export declare const PACKAGE_VERSION = "0.1.3-beta";
|
|
2
2
|
export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "0.1.
|
|
1
|
+
export const PACKAGE_VERSION = "0.1.3-beta";
|
|
2
2
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export const DEFAULT_IGNORE_DIRECTORIES = new Set([
|
|
@@ -1,13 +1,44 @@
|
|
|
1
1
|
import { parseProjectMcpConfigs } from "../mcp/parse.js";
|
|
2
|
-
import { createIgnoreIndex, parseIgnoreFile } from "./ignore.js";
|
|
2
|
+
import { createIgnoreIndex, parseIgnoreFile, relativizeIgnorePatterns } from "./ignore.js";
|
|
3
3
|
import { TextCache } from "./text-cache.js";
|
|
4
4
|
export async function buildRuleContext(options) {
|
|
5
5
|
const textCache = new TextCache(options.root, options.maxFileSizeBytes);
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const gitignorePatterns = [];
|
|
7
|
+
const cursorignorePatterns = [];
|
|
8
|
+
const rootGitignore = await textCache.read(".gitignore");
|
|
9
|
+
if (rootGitignore.text) {
|
|
10
|
+
gitignorePatterns.push(...parseIgnoreFile(rootGitignore.text));
|
|
11
|
+
}
|
|
12
|
+
const rootCursorignore = await textCache.read(".cursorignore");
|
|
13
|
+
if (rootCursorignore.text) {
|
|
14
|
+
cursorignorePatterns.push(...parseIgnoreFile(rootCursorignore.text));
|
|
15
|
+
}
|
|
16
|
+
for (const file of options.discovery.files) {
|
|
17
|
+
const base = file.relativePath.split("/").pop() ?? file.relativePath;
|
|
18
|
+
if (base !== ".gitignore" && base !== ".cursorignore") {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (file.relativePath === ".gitignore" || file.relativePath === ".cursorignore") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const cached = await textCache.read(file.relativePath);
|
|
25
|
+
if (!cached.text) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const dir = file.relativePath.includes("/")
|
|
29
|
+
? file.relativePath.slice(0, file.relativePath.lastIndexOf("/"))
|
|
30
|
+
: "";
|
|
31
|
+
const patterns = relativizeIgnorePatterns(dir, parseIgnoreFile(cached.text));
|
|
32
|
+
if (base === ".gitignore") {
|
|
33
|
+
gitignorePatterns.push(...patterns);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
cursorignorePatterns.push(...patterns);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
8
39
|
const ignore = createIgnoreIndex({
|
|
9
|
-
gitignorePatterns
|
|
10
|
-
cursorignorePatterns
|
|
40
|
+
gitignorePatterns,
|
|
41
|
+
cursorignorePatterns,
|
|
11
42
|
});
|
|
12
43
|
const mcp = await parseProjectMcpConfigs(options.root, options.maxFileSizeBytes);
|
|
13
44
|
const instructionPaths = new Set();
|
|
@@ -9,6 +9,13 @@ const GENERATED = [
|
|
|
9
9
|
{ name: ".dart_tool", label: "Dart tooling cache" },
|
|
10
10
|
{ name: "target", label: "Rust/Java build output" },
|
|
11
11
|
];
|
|
12
|
+
function isGeneratedPath(relativePath, name) {
|
|
13
|
+
return relativePath === name || relativePath.endsWith(`/${name}`);
|
|
14
|
+
}
|
|
15
|
+
function isExcluded(context, relativePath) {
|
|
16
|
+
const variants = [relativePath, `${relativePath}/`];
|
|
17
|
+
return variants.some((candidate) => context.ignore.matchesGitignore(candidate) || context.ignore.matchesCursorignore(candidate));
|
|
18
|
+
}
|
|
12
19
|
export const generatedDirectoryRule = {
|
|
13
20
|
id: "context/generated-directory",
|
|
14
21
|
title: "Generated directory may enter agent context",
|
|
@@ -20,66 +27,49 @@ export const generatedDirectoryRule = {
|
|
|
20
27
|
recommendation: "Ensure generated directories are listed in .gitignore and agent ignore configuration where applicable.",
|
|
21
28
|
async check(context) {
|
|
22
29
|
const findings = [];
|
|
23
|
-
const skipped = new Set(context.discovery.directoriesSkipped.map((d) => d.split("/")[0] ?? d));
|
|
24
|
-
// directoriesSkipped stores relative paths like "dist" or "packages/app/dist"
|
|
25
|
-
const present = new Set();
|
|
26
|
-
for (const dir of context.discovery.directoriesSkipped) {
|
|
27
|
-
const top = dir.split("/")[0] ?? dir;
|
|
28
|
-
present.add(top);
|
|
29
|
-
present.add(dir);
|
|
30
|
-
}
|
|
31
|
-
void skipped;
|
|
32
30
|
const affected = context.agents.filter((a) => a.configured || a.detected).map((a) => a.id);
|
|
33
31
|
if (affected.length === 0) {
|
|
34
32
|
return [];
|
|
35
33
|
}
|
|
36
34
|
for (const entry of GENERATED) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
35
|
+
const matches = context.discovery.directoriesSkipped.filter((dir) => isGeneratedPath(dir, entry.name));
|
|
36
|
+
for (const relativePath of matches) {
|
|
37
|
+
if (isExcluded(context, relativePath)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
findings.push({
|
|
41
|
+
ruleId: "context/generated-directory",
|
|
42
|
+
category: "context",
|
|
43
|
+
severity: "info",
|
|
44
|
+
title: "Generated directory may enter agent context",
|
|
45
|
+
message: `${relativePath}/ (${entry.label}) is present and no project ignore pattern was detected`,
|
|
46
|
+
whyItMatters: "Generated directories are usually low-value for coding agents and can bloat indexing/context when not excluded.",
|
|
47
|
+
recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and, for Cursor, .cursorignore if you need agent-specific exclusion beyond gitignore.`,
|
|
48
|
+
affectedAgents: affected,
|
|
49
|
+
evidence: { path: relativePath },
|
|
50
|
+
fixability: "safe",
|
|
51
|
+
});
|
|
50
52
|
}
|
|
51
|
-
// Discovery already skips these dirs by default — absence from ignore files is still useful info
|
|
52
|
-
// when agents are configured. Keep severity info.
|
|
53
|
-
findings.push({
|
|
54
|
-
ruleId: "context/generated-directory",
|
|
55
|
-
category: "context",
|
|
56
|
-
severity: "info",
|
|
57
|
-
title: "Generated directory may enter agent context",
|
|
58
|
-
message: `${entry.name}/ (${entry.label}) is present and no project ignore pattern was detected`,
|
|
59
|
-
whyItMatters: "Generated directories are usually low-value for coding agents and can bloat indexing/context when not excluded.",
|
|
60
|
-
recommendation: `Add ${entry.name}/ to .gitignore and, for Cursor, .cursorignore if you need agent-specific exclusion beyond gitignore.`,
|
|
61
|
-
affectedAgents: affected,
|
|
62
|
-
evidence: { path: entry.name },
|
|
63
|
-
fixability: "safe",
|
|
64
|
-
});
|
|
65
53
|
}
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
[...present].some((p) => p === "vendor" || p.endsWith("/vendor"))) &&
|
|
54
|
+
const vendorPaths = context.discovery.directoriesSkipped.filter((dir) => isGeneratedPath(dir, "vendor"));
|
|
55
|
+
if (vendorPaths.length > 0 &&
|
|
69
56
|
context.repository.primaryPackageManager !== "composer" &&
|
|
57
|
+
!context.repository.packageManagers.includes("composer") &&
|
|
70
58
|
context.repository.primaryLanguage !== "php") {
|
|
71
|
-
const
|
|
72
|
-
|
|
59
|
+
for (const relativePath of vendorPaths) {
|
|
60
|
+
if (isExcluded(context, relativePath)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
73
63
|
findings.push({
|
|
74
64
|
ruleId: "context/generated-directory",
|
|
75
65
|
category: "context",
|
|
76
66
|
severity: "info",
|
|
77
67
|
title: "Generated directory may enter agent context",
|
|
78
|
-
message:
|
|
68
|
+
message: `${relativePath}/ is present outside a PHP/Composer project and no ignore pattern was detected`,
|
|
79
69
|
whyItMatters: "In non-PHP projects, vendor/ is often third-party or generated content that adds noise to agent context.",
|
|
80
70
|
recommendation: "Confirm whether vendor/ should be ignored for git and AI tooling.",
|
|
81
71
|
affectedAgents: affected,
|
|
82
|
-
evidence: { path:
|
|
72
|
+
evidence: { path: relativePath },
|
|
83
73
|
fixability: "safe",
|
|
84
74
|
});
|
|
85
75
|
}
|
|
@@ -11,6 +11,11 @@ export interface IgnoreIndex {
|
|
|
11
11
|
isExcludedForCursor(relativePath: string): boolean;
|
|
12
12
|
}
|
|
13
13
|
export declare function parseIgnoreFile(content: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Rewrite patterns from a nested ignore file so they apply relative to repository root.
|
|
16
|
+
* `baseDir` is the directory containing the ignore file (POSIX, no trailing slash), or "".
|
|
17
|
+
*/
|
|
18
|
+
export declare function relativizeIgnorePatterns(baseDir: string, patterns: string[]): string[];
|
|
14
19
|
/**
|
|
15
20
|
* Match a relative POSIX path against a single gitignore-like pattern.
|
|
16
21
|
* Negation (`!`) is not fully supported for re-inclusion of parent-excluded paths.
|
|
@@ -15,6 +15,30 @@ export function parseIgnoreFile(content) {
|
|
|
15
15
|
.map(normalizePattern)
|
|
16
16
|
.filter((p) => p !== null);
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Rewrite patterns from a nested ignore file so they apply relative to repository root.
|
|
20
|
+
* `baseDir` is the directory containing the ignore file (POSIX, no trailing slash), or "".
|
|
21
|
+
*/
|
|
22
|
+
export function relativizeIgnorePatterns(baseDir, patterns) {
|
|
23
|
+
if (!baseDir || baseDir === ".") {
|
|
24
|
+
return patterns;
|
|
25
|
+
}
|
|
26
|
+
return patterns.map((pattern) => {
|
|
27
|
+
const negated = pattern.startsWith("!");
|
|
28
|
+
const body = negated ? pattern.slice(1) : pattern;
|
|
29
|
+
let rewritten;
|
|
30
|
+
if (body.startsWith("/")) {
|
|
31
|
+
rewritten = `${baseDir}${body}`;
|
|
32
|
+
}
|
|
33
|
+
else if (body.startsWith("**/")) {
|
|
34
|
+
rewritten = `${baseDir}/${body.slice(3)}`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
rewritten = `${baseDir}/${body}`;
|
|
38
|
+
}
|
|
39
|
+
return negated ? `!${rewritten}` : rewritten;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
18
42
|
/**
|
|
19
43
|
* Match a relative POSIX path against a single gitignore-like pattern.
|
|
20
44
|
* Negation (`!`) is not fully supported for re-inclusion of parent-excluded paths.
|
|
@@ -24,25 +48,44 @@ export function matchIgnorePattern(relativePath, pattern) {
|
|
|
24
48
|
if (pat.startsWith("!")) {
|
|
25
49
|
pat = pat.slice(1);
|
|
26
50
|
}
|
|
27
|
-
const pathNorm = relativePath.replace(/^\/+/, "");
|
|
51
|
+
const pathNorm = relativePath.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
28
52
|
let matched = false;
|
|
29
53
|
if (pat.endsWith("/")) {
|
|
30
54
|
const dir = pat.slice(0, -1);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
if (!dir.includes("/")) {
|
|
56
|
+
// `build/` matches build at any directory level (gitignore semantics)
|
|
57
|
+
matched =
|
|
58
|
+
pathNorm === dir ||
|
|
59
|
+
pathNorm.endsWith(`/${dir}`) ||
|
|
60
|
+
pathNorm.split("/").includes(dir) ||
|
|
61
|
+
globMatch(pathNorm, `**/${dir}`) ||
|
|
62
|
+
globMatch(pathNorm, `**/${dir}/**`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
matched =
|
|
66
|
+
pathNorm === dir ||
|
|
67
|
+
pathNorm.startsWith(`${dir}/`) ||
|
|
68
|
+
globMatch(pathNorm, dir) ||
|
|
69
|
+
globMatch(pathNorm, `${dir}/**`);
|
|
70
|
+
}
|
|
36
71
|
}
|
|
37
72
|
else if (pat.startsWith("/")) {
|
|
38
73
|
matched = globMatch(pathNorm, pat.slice(1));
|
|
39
74
|
}
|
|
40
75
|
else if (pat.includes("/")) {
|
|
41
|
-
matched =
|
|
76
|
+
matched =
|
|
77
|
+
globMatch(pathNorm, pat) ||
|
|
78
|
+
globMatch(pathNorm, `${pat}/**`) ||
|
|
79
|
+
pathNorm.endsWith(`/${pat}`) ||
|
|
80
|
+
pathNorm.startsWith(`${pat}/`);
|
|
42
81
|
}
|
|
43
82
|
else {
|
|
44
83
|
const base = pathNorm.split("/").pop() ?? pathNorm;
|
|
45
|
-
matched =
|
|
84
|
+
matched =
|
|
85
|
+
globMatch(base, pat) ||
|
|
86
|
+
globMatch(pathNorm, pat) ||
|
|
87
|
+
globMatch(pathNorm, `**/${pat}`) ||
|
|
88
|
+
pathNorm.split("/").includes(pat);
|
|
46
89
|
}
|
|
47
90
|
return matched;
|
|
48
91
|
}
|
|
@@ -90,7 +133,6 @@ export function createIgnoreIndex(options) {
|
|
|
90
133
|
if (pathMatchesAny(relativePath, gitignorePatterns)) {
|
|
91
134
|
return true;
|
|
92
135
|
}
|
|
93
|
-
// Documented default ignore list includes .env*
|
|
94
136
|
return pathMatchesAny(relativePath, CURSOR_DEFAULT_ENV_PATTERNS);
|
|
95
137
|
},
|
|
96
138
|
};
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { RuleDefinition } from "../types.js";
|
|
2
|
+
export type EnvFileKind = "runtime" | "template" | "backup";
|
|
3
|
+
/**
|
|
4
|
+
* Classify environment-like filenames. Precision over recall.
|
|
5
|
+
* Does not inspect file contents.
|
|
6
|
+
*/
|
|
7
|
+
export declare function classifyEnvBasename(base: string): EnvFileKind | null;
|
|
2
8
|
export declare const envFileExposureRule: RuleDefinition;
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
const
|
|
1
|
+
const TEMPLATE_BASENAMES = new Set([".env.example", ".env.sample", ".env.template", ".env.dist"]);
|
|
2
|
+
const BACKUP_BASENAMES = new Set([".env_backup", ".env_old", ".env_local"]);
|
|
3
|
+
/**
|
|
4
|
+
* Classify environment-like filenames. Precision over recall.
|
|
5
|
+
* Does not inspect file contents.
|
|
6
|
+
*/
|
|
7
|
+
export function classifyEnvBasename(base) {
|
|
8
|
+
const normalized = base.trim();
|
|
9
|
+
if (!normalized.toLowerCase().startsWith(".env")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const lower = normalized.toLowerCase();
|
|
13
|
+
if (TEMPLATE_BASENAMES.has(lower)) {
|
|
14
|
+
return "template";
|
|
15
|
+
}
|
|
16
|
+
if (BACKUP_BASENAMES.has(lower)) {
|
|
17
|
+
return "backup";
|
|
18
|
+
}
|
|
19
|
+
if (lower === ".env") {
|
|
20
|
+
return "runtime";
|
|
21
|
+
}
|
|
22
|
+
// .env.<name> but not templates above
|
|
23
|
+
if (/^\.env\.[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
24
|
+
return "runtime";
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
2
28
|
function isEnvFile(relativePath) {
|
|
3
29
|
const base = relativePath.split("/").pop() ?? relativePath;
|
|
4
|
-
return
|
|
30
|
+
return classifyEnvBasename(base);
|
|
31
|
+
}
|
|
32
|
+
function hasConfiguredOrDetectedAgents(context) {
|
|
33
|
+
return context.agents.some((a) => a.detected || a.configured);
|
|
5
34
|
}
|
|
6
35
|
function agentsWithoutClearExclusion(context, relativePath) {
|
|
7
36
|
const affected = [];
|
|
@@ -50,16 +79,53 @@ async function claudeDeniesRead(context, relativePath) {
|
|
|
50
79
|
export const envFileExposureRule = {
|
|
51
80
|
id: "security/env-file-exposure",
|
|
52
81
|
title: "Sensitive environment file may enter agent context",
|
|
53
|
-
description: "Detects repository .env files
|
|
82
|
+
description: "Detects repository .env files as repository risk and, when agents are configured, possible agent context exposure.",
|
|
54
83
|
category: "security",
|
|
55
84
|
severity: "critical",
|
|
56
85
|
fixability: "review",
|
|
57
|
-
rationale: "Environment files often contain credentials.
|
|
58
|
-
recommendation: "
|
|
86
|
+
rationale: "Environment files often contain credentials. Repository presence is a risk; agent readability can add exposure.",
|
|
87
|
+
recommendation: "Keep runtime .env files out of version control, exclude them from AI agent context, and rotate credentials if exposed.",
|
|
59
88
|
async check(context) {
|
|
60
89
|
const findings = [];
|
|
61
|
-
const
|
|
62
|
-
for (const file of
|
|
90
|
+
const agentsPresent = hasConfiguredOrDetectedAgents(context);
|
|
91
|
+
for (const file of context.discovery.files) {
|
|
92
|
+
const kind = isEnvFile(file.relativePath);
|
|
93
|
+
if (!kind) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (kind === "template") {
|
|
97
|
+
findings.push({
|
|
98
|
+
ruleId: "security/env-file-exposure",
|
|
99
|
+
category: "security",
|
|
100
|
+
severity: "info",
|
|
101
|
+
title: "Environment template file present",
|
|
102
|
+
message: `Environment template file detected at ${file.relativePath}`,
|
|
103
|
+
whyItMatters: "Template env files are usually placeholders, but they can still document secret names. Filename classification only — contents were not inspected.",
|
|
104
|
+
recommendation: "Keep templates free of real credentials. Prefer documenting variable names without values.",
|
|
105
|
+
affectedAgents: [],
|
|
106
|
+
evidence: { path: file.relativePath, detail: "template" },
|
|
107
|
+
fixability: "review",
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!agentsPresent) {
|
|
112
|
+
findings.push({
|
|
113
|
+
ruleId: "security/env-file-exposure",
|
|
114
|
+
category: "security",
|
|
115
|
+
severity: kind === "backup" ? "warning" : "critical",
|
|
116
|
+
title: "Sensitive environment file present in repository",
|
|
117
|
+
message: `Sensitive environment file present in the repository: ${file.relativePath}`,
|
|
118
|
+
whyItMatters: "No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. The file is still high-risk repository material if tracked or shared.",
|
|
119
|
+
recommendation: "Keep runtime environment files out of version control and rotate credentials if the file may have been shared.",
|
|
120
|
+
affectedAgents: [],
|
|
121
|
+
evidence: {
|
|
122
|
+
path: file.relativePath,
|
|
123
|
+
detail: kind === "backup" ? "backup-no-agent" : "runtime-no-agent",
|
|
124
|
+
},
|
|
125
|
+
fixability: "review",
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
63
129
|
let affected = agentsWithoutClearExclusion(context, file.relativePath);
|
|
64
130
|
if (affected.includes("claude-code")) {
|
|
65
131
|
if (await claudeDeniesRead(context, file.relativePath)) {
|
|
@@ -72,13 +138,16 @@ export const envFileExposureRule = {
|
|
|
72
138
|
findings.push({
|
|
73
139
|
ruleId: "security/env-file-exposure",
|
|
74
140
|
category: "security",
|
|
75
|
-
severity: "critical",
|
|
141
|
+
severity: kind === "backup" ? "warning" : "critical",
|
|
76
142
|
title: "Sensitive environment file may enter agent context",
|
|
77
143
|
message: `Sensitive environment file detected and no relevant exclusion was found for: ${file.relativePath}`,
|
|
78
144
|
whyItMatters: "Environment files frequently hold API keys and credentials. If readable by an AI coding agent, those values may be included in prompts or logs.",
|
|
79
145
|
recommendation: "Add an agent-specific exclusion (for example .cursorignore or a Claude Code Read deny rule), keep the file out of version control, and rotate any credentials that may have been exposed.",
|
|
80
146
|
affectedAgents: affected,
|
|
81
|
-
evidence: {
|
|
147
|
+
evidence: {
|
|
148
|
+
path: file.relativePath,
|
|
149
|
+
detail: kind === "backup" ? "backup" : "runtime",
|
|
150
|
+
},
|
|
82
151
|
fixability: "review",
|
|
83
152
|
});
|
|
84
153
|
}
|
|
@@ -6,10 +6,14 @@ const PRIVATE_KEY_PATTERNS = [
|
|
|
6
6
|
test: (b) => /\.pem$/i.test(b) && !/cert|certificate|public|ca-bundle/i.test(b),
|
|
7
7
|
label: "PEM private key",
|
|
8
8
|
},
|
|
9
|
+
{
|
|
10
|
+
test: (b) => /\.der$/i.test(b) && !/cert|certificate|public|ca-bundle/i.test(b),
|
|
11
|
+
label: "DER-encoded key or credential material",
|
|
12
|
+
},
|
|
9
13
|
{ test: (b) => /\.p12$/i.test(b), label: "PKCS#12 credential bundle" },
|
|
10
14
|
{ test: (b) => /\.pfx$/i.test(b), label: "PKCS#12 credential bundle" },
|
|
11
15
|
{ test: (b) => /^credentials\.json$/i.test(b), label: "credentials file" },
|
|
12
|
-
{ test: (b) => /service-account.*\.json$/i.test(b), label: "service account key" },
|
|
16
|
+
{ test: (b) => /service[-_]?account.*\.json$/i.test(b), label: "service account key" },
|
|
13
17
|
{
|
|
14
18
|
test: (b) => /\.key$/i.test(b) && !/public|pub/i.test(b),
|
|
15
19
|
label: "private key file",
|
|
@@ -35,22 +39,24 @@ export const privateKeyFileRule = {
|
|
|
35
39
|
async check(context) {
|
|
36
40
|
const findings = [];
|
|
37
41
|
const affected = context.agents.filter((a) => a.detected || a.configured).map((a) => a.id);
|
|
38
|
-
const agents = affected.length > 0 ? affected : ["cursor", "claude-code", "codex"];
|
|
39
42
|
for (const file of context.discovery.files) {
|
|
40
43
|
const base = file.relativePath.split("/").pop() ?? file.relativePath;
|
|
41
44
|
const label = classify(base);
|
|
42
45
|
if (!label) {
|
|
43
46
|
continue;
|
|
44
47
|
}
|
|
48
|
+
const agentsPresent = affected.length > 0;
|
|
45
49
|
findings.push({
|
|
46
50
|
ruleId: "security/private-key-file",
|
|
47
51
|
category: "security",
|
|
48
52
|
severity: "critical",
|
|
49
53
|
title: "Private key or credential file present in repository",
|
|
50
54
|
message: `Possible ${label} detected at ${file.relativePath}`,
|
|
51
|
-
whyItMatters:
|
|
55
|
+
whyItMatters: agentsPresent
|
|
56
|
+
? "Credential material in the working tree can be read by AI coding agents and may leak into model context or logs. Filename heuristics only — contents were not inspected."
|
|
57
|
+
: "Credential material in the working tree is high-risk repository content. No supported coding-agent configuration was detected, so agent-specific exposure was not asserted. Filename heuristics only — contents were not inspected.",
|
|
52
58
|
recommendation: "Remove the file from the repository, rotate the credential, add ignore rules, and never commit replacements.",
|
|
53
|
-
affectedAgents:
|
|
59
|
+
affectedAgents: affected,
|
|
54
60
|
evidence: { path: file.relativePath, detail: label },
|
|
55
61
|
fixability: "manual",
|
|
56
62
|
});
|
|
@@ -46,6 +46,11 @@ export async function scan(options = {}) {
|
|
|
46
46
|
for (const error of ruleErrors) {
|
|
47
47
|
warnings.push(sanitizeTerminalText(error));
|
|
48
48
|
}
|
|
49
|
+
const agentsPresent = agents.some((a) => a.detected || a.configured);
|
|
50
|
+
const agentSecurityAnalysis = agentsPresent ? "full" : "limited";
|
|
51
|
+
if (agentSecurityAnalysis === "limited") {
|
|
52
|
+
warnings.push("No supported coding-agent configuration detected; agent-specific security exposure checks are limited.");
|
|
53
|
+
}
|
|
49
54
|
return {
|
|
50
55
|
version: PACKAGE_VERSION,
|
|
51
56
|
repository,
|
|
@@ -54,6 +59,7 @@ export async function scan(options = {}) {
|
|
|
54
59
|
summary,
|
|
55
60
|
scores: null,
|
|
56
61
|
scoringAvailable: false,
|
|
62
|
+
agentSecurityAnalysis,
|
|
57
63
|
timing: {
|
|
58
64
|
discoveryMs: discovery.elapsedMs,
|
|
59
65
|
detectionMs,
|
|
@@ -3,6 +3,8 @@ export interface FrameworkDetectionInput {
|
|
|
3
3
|
relativePaths: string[];
|
|
4
4
|
packageJsonDependencies?: Record<string, string>;
|
|
5
5
|
composerJsonRequire?: Record<string, string>;
|
|
6
|
+
/** Lowercase dependency/package names discovered from Python manifests. */
|
|
7
|
+
pythonDependencyNames?: string[];
|
|
6
8
|
pubspecName?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface FrameworkDetectionResult {
|
|
@@ -15,3 +17,4 @@ export interface FrameworkDetectionResult {
|
|
|
15
17
|
*/
|
|
16
18
|
export declare function detectFrameworks(input: FrameworkDetectionInput): FrameworkDetectionResult;
|
|
17
19
|
export declare function formatFramework(framework: FrameworkId): string;
|
|
20
|
+
export declare function formatFrameworks(ids: FrameworkId[]): string;
|