@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 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.1-beta...HEAD
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
  [![GitHub Stars](https://img.shields.io/github/stars/pranee54/AgentDoctor)](https://github.com/pranee54/AgentDoctor/stargazers)
6
6
  [![CI](https://img.shields.io/github/actions/workflow/status/pranee54/AgentDoctor/ci.yml?branch=main&label=CI)](https://github.com/pranee54/AgentDoctor/actions)
7
7
  [![Node](https://img.shields.io/node/v/@praneeth_54/agentdoctor)](https://nodejs.org)
8
- [![Release](https://img.shields.io/badge/release-v0.1.1--beta-orange)](CHANGELOG.md)
8
+ [![Release](https://img.shields.io/badge/release-v0.1.2--beta-orange)](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.1-beta
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.1-beta
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.1-beta
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.1-beta",
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
@@ -1,4 +1,4 @@
1
- export declare const PACKAGE_VERSION = "0.1.1-beta";
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-beta";
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 gitignore = await textCache.read(".gitignore");
7
- const cursorignore = await textCache.read(".cursorignore");
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: gitignore.text ? parseIgnoreFile(gitignore.text) : [],
10
- cursorignorePatterns: cursorignore.text ? parseIgnoreFile(cursorignore.text) : [],
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 found = present.has(entry.name) ||
38
- [...present].some((p) => p === entry.name || p.endsWith(`/${entry.name}`));
39
- if (!found) {
40
- continue;
41
- }
42
- // If .gitignore or .cursorignore already exclude it, skip (already appropriately excluded)
43
- const samplePath = `${entry.name}/`;
44
- const excluded = context.ignore.matchesGitignore(samplePath) ||
45
- context.ignore.matchesGitignore(entry.name) ||
46
- context.ignore.matchesCursorignore(samplePath) ||
47
- context.ignore.matchesCursorignore(entry.name);
48
- if (excluded) {
49
- continue;
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
- // vendor/ — only flag for non-PHP/Composer ecosystems
67
- if ((present.has("vendor") ||
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 excluded = context.ignore.matchesGitignore("vendor/") || context.ignore.matchesCursorignore("vendor/");
72
- if (!excluded) {
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: "vendor/ is present outside a PHP/Composer project and no ignore pattern was detected",
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: "vendor" },
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
- matched =
32
- pathNorm === dir ||
33
- pathNorm.startsWith(`${dir}/`) ||
34
- globMatch(pathNorm, dir) ||
35
- pathNorm.split("/").some((_, i, parts) => globMatch(parts.slice(0, i + 1).join("/"), dir));
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 = globMatch(pathNorm, pat) || pathNorm.endsWith(`/${pat}`);
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 = globMatch(base, pat) || globMatch(pathNorm, pat) || globMatch(pathNorm, `**/${pat}`);
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 ENV_BASENAME = /^\.env(\..+)?$/i;
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 ENV_BASENAME.test(base);
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 when configured AI agents may be able to access them.",
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. If an AI coding agent can read them, secrets may enter model context.",
58
- recommendation: "Exclude the file from AI agent context (for example .cursorignore / Claude Code Read deny rules) and ensure it is not committed.",
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 candidates = context.discovery.files.filter((f) => isEnvFile(f.relativePath));
62
- for (const file of candidates) {
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: { path: file.relativePath },
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: "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.",
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: [...agents],
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;