@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
|
@@ -4,6 +4,9 @@ function hasDep(deps, name) {
|
|
|
4
4
|
function hasFile(paths, ...names) {
|
|
5
5
|
return names.some((name) => paths.has(name.toLowerCase()));
|
|
6
6
|
}
|
|
7
|
+
function hasPythonDep(names, dep) {
|
|
8
|
+
return Boolean(names?.includes(dep.toLowerCase()));
|
|
9
|
+
}
|
|
7
10
|
/**
|
|
8
11
|
* Detect frameworks from config markers and dependency names.
|
|
9
12
|
* Does not fail when nothing matches — returns unknown.
|
|
@@ -18,6 +21,7 @@ export function detectFrameworks(input) {
|
|
|
18
21
|
...(input.packageJsonDependencies ?? {}),
|
|
19
22
|
};
|
|
20
23
|
const composer = input.composerJsonRequire ?? {};
|
|
24
|
+
const pythonDeps = input.pythonDependencyNames ?? [];
|
|
21
25
|
const detected = [];
|
|
22
26
|
const isNext = hasFile(basenames, "next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs") ||
|
|
23
27
|
hasDep(deps, "next");
|
|
@@ -63,15 +67,12 @@ export function detectFrameworks(input) {
|
|
|
63
67
|
!isVue &&
|
|
64
68
|
!isSvelte &&
|
|
65
69
|
!isReact &&
|
|
66
|
-
(hasFile(basenames, "package.json") || hasDep(deps, "express") === false) &&
|
|
67
70
|
hasFile(basenames, "package.json")) {
|
|
68
|
-
// Generic Node.js project with package.json but no web framework
|
|
69
71
|
if (!detected.includes("nodejs")) {
|
|
70
72
|
detected.push("nodejs");
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
if (hasFile(basenames, "pubspec.yaml") || input.pubspecName !== undefined) {
|
|
74
|
-
// Flutter: pubspec presence implies Flutter/Dart project context for detection
|
|
75
76
|
if (hasFile(pathSet, "pubspec.yaml") || basenames.has("pubspec.yaml")) {
|
|
76
77
|
detected.push("flutter");
|
|
77
78
|
}
|
|
@@ -79,17 +80,19 @@ export function detectFrameworks(input) {
|
|
|
79
80
|
if (hasDep(composer, "laravel/framework") || hasFile(basenames, "artisan")) {
|
|
80
81
|
detected.push("laravel");
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const isDjango = hasFile(basenames, "manage.py") ||
|
|
84
|
+
hasPythonDep(pythonDeps, "django") ||
|
|
85
|
+
[...pathSet].some((p) => p.endsWith("/requirements-django.txt"));
|
|
86
|
+
if (isDjango) {
|
|
84
87
|
detected.push("django");
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const hasFastApiDep = hasPythonDep(pythonDeps, "fastapi") ||
|
|
90
|
+
[...pathSet].some((p) => p.includes("fastapi") || p.endsWith("requirements-fastapi.txt"));
|
|
91
|
+
const hasFastApiAppLayout = [...pathSet].some((p) => p === "app/main.py" || p.endsWith("/app/main.py"));
|
|
92
|
+
if (hasFastApiDep && hasFastApiAppLayout) {
|
|
88
93
|
detected.push("fastapi");
|
|
89
94
|
}
|
|
90
|
-
// Deduplicate while preserving order
|
|
91
95
|
const unique = [...new Set(detected)];
|
|
92
|
-
// Priority for primary framework
|
|
93
96
|
const priority = [
|
|
94
97
|
"nextjs",
|
|
95
98
|
"nuxt",
|
|
@@ -146,3 +149,10 @@ export function formatFramework(framework) {
|
|
|
146
149
|
return "Unknown";
|
|
147
150
|
}
|
|
148
151
|
}
|
|
152
|
+
export function formatFrameworks(ids) {
|
|
153
|
+
const unique = [...new Set(ids)].filter((id) => id !== "unknown");
|
|
154
|
+
if (unique.length === 0) {
|
|
155
|
+
return "Unknown";
|
|
156
|
+
}
|
|
157
|
+
return unique.map((id) => formatFramework(id)).join(", ");
|
|
158
|
+
}
|
|
@@ -12,3 +12,4 @@ export interface LanguageDetectionResult {
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function detectLanguages(input: LanguageDetectionInput): LanguageDetectionResult;
|
|
14
14
|
export declare function formatLanguage(language: LanguageId): string;
|
|
15
|
+
export declare function formatLanguages(ids: LanguageId[]): string;
|
|
@@ -120,3 +120,10 @@ export function formatLanguage(language) {
|
|
|
120
120
|
return "Unknown";
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
export function formatLanguages(ids) {
|
|
124
|
+
const unique = [...new Set(ids)].filter((id) => id !== "unknown");
|
|
125
|
+
if (unique.length === 0) {
|
|
126
|
+
return "Unknown";
|
|
127
|
+
}
|
|
128
|
+
return unique.map((id) => formatLanguage(id)).join(", ");
|
|
129
|
+
}
|
|
@@ -7,7 +7,8 @@ export interface MonorepoDetectionResult {
|
|
|
7
7
|
monorepo: MonorepoToolId;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Detect common monorepo tooling from config markers
|
|
10
|
+
* Detect common monorepo tooling from config markers, or multi-project layouts
|
|
11
|
+
* with multiple independently buildable manifests.
|
|
11
12
|
*/
|
|
12
13
|
export declare function detectMonorepo(input: MonorepoDetectionInput): MonorepoDetectionResult;
|
|
13
14
|
export declare function formatMonorepo(id: MonorepoToolId): string;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
const PROJECT_MANIFESTS = new Set([
|
|
2
|
+
"package.json",
|
|
3
|
+
"pubspec.yaml",
|
|
4
|
+
"composer.json",
|
|
5
|
+
"cargo.toml",
|
|
6
|
+
"go.mod",
|
|
7
|
+
"pyproject.toml",
|
|
8
|
+
"gemfile",
|
|
9
|
+
]);
|
|
1
10
|
/**
|
|
2
|
-
* Detect common monorepo tooling from config markers
|
|
11
|
+
* Detect common monorepo tooling from config markers, or multi-project layouts
|
|
12
|
+
* with multiple independently buildable manifests.
|
|
3
13
|
*/
|
|
4
14
|
export function detectMonorepo(input) {
|
|
5
15
|
const basenames = new Set(input.relativePaths.map((p) => {
|
|
@@ -26,6 +36,25 @@ export function detectMonorepo(input) {
|
|
|
26
36
|
return { monorepo: "npm-workspaces" };
|
|
27
37
|
}
|
|
28
38
|
}
|
|
39
|
+
const projectRoots = new Set();
|
|
40
|
+
for (const relativePath of input.relativePaths) {
|
|
41
|
+
const parts = relativePath.split("/");
|
|
42
|
+
const base = (parts[parts.length - 1] ?? relativePath).toLowerCase();
|
|
43
|
+
if (!PROJECT_MANIFESTS.has(base)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (parts.length === 1) {
|
|
47
|
+
projectRoots.add(".");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Count only first-level project directories (e.g. mobile-apps/pubspec.yaml)
|
|
51
|
+
if (parts.length === 2) {
|
|
52
|
+
projectRoots.add(parts[0] ?? relativePath);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (projectRoots.size >= 2) {
|
|
56
|
+
return { monorepo: "multi-project" };
|
|
57
|
+
}
|
|
29
58
|
return { monorepo: "none" };
|
|
30
59
|
}
|
|
31
60
|
export function formatMonorepo(id) {
|
|
@@ -38,6 +67,8 @@ export function formatMonorepo(id) {
|
|
|
38
67
|
return "Turborepo";
|
|
39
68
|
case "nx":
|
|
40
69
|
return "Nx";
|
|
70
|
+
case "multi-project":
|
|
71
|
+
return "Multi-project";
|
|
41
72
|
default:
|
|
42
73
|
return "None";
|
|
43
74
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { PackageManagerId } from "../types/index.js";
|
|
2
2
|
export interface PackageManagerDetectionInput {
|
|
3
3
|
relativePaths: string[];
|
|
4
|
+
/** True when a pyproject.toml contains a [tool.poetry] table. */
|
|
5
|
+
poetryToolDetected?: boolean;
|
|
6
|
+
/** True when a non-Poetry pyproject.toml (or similar) indicates a Python project. */
|
|
7
|
+
genericPyprojectDetected?: boolean;
|
|
4
8
|
}
|
|
5
9
|
export interface PackageManagerDetectionResult {
|
|
6
10
|
packageManagers: PackageManagerId[];
|
|
@@ -8,7 +12,9 @@ export interface PackageManagerDetectionResult {
|
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* Detect package managers from lockfiles and manifests.
|
|
11
|
-
*
|
|
15
|
+
* Primary selection prefers root-level evidence and avoids inventing certainty for mixed ecosystems.
|
|
16
|
+
* Poetry requires poetry.lock and/or [tool.poetry] — not bare pyproject.toml.
|
|
12
17
|
*/
|
|
13
18
|
export declare function detectPackageManagers(input: PackageManagerDetectionInput): PackageManagerDetectionResult;
|
|
14
19
|
export declare function formatPackageManager(id: PackageManagerId): string;
|
|
20
|
+
export declare function formatPackageManagers(ids: PackageManagerId[]): string;
|
|
@@ -1,32 +1,121 @@
|
|
|
1
1
|
const MARKERS = [
|
|
2
|
-
{ id: "pnpm", files: ["pnpm-lock.yaml", "pnpm-workspace.yaml"] },
|
|
3
|
-
{ id: "yarn", files: ["yarn.lock", ".yarnrc.yml"] },
|
|
4
|
-
{ id: "bun", files: ["bun.lockb", "bun.lock"] },
|
|
5
|
-
{
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
{ id: "
|
|
2
|
+
{ id: "pnpm", files: ["pnpm-lock.yaml", "pnpm-workspace.yaml"], lockfiles: ["pnpm-lock.yaml"] },
|
|
3
|
+
{ id: "yarn", files: ["yarn.lock", ".yarnrc.yml"], lockfiles: ["yarn.lock"] },
|
|
4
|
+
{ id: "bun", files: ["bun.lockb", "bun.lock"], lockfiles: ["bun.lockb", "bun.lock"] },
|
|
5
|
+
{
|
|
6
|
+
id: "npm",
|
|
7
|
+
files: ["package-lock.json", "npm-shrinkwrap.json", "package.json"],
|
|
8
|
+
lockfiles: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
9
|
+
},
|
|
10
|
+
{ id: "composer", files: ["composer.lock", "composer.json"], lockfiles: ["composer.lock"] },
|
|
11
|
+
{ id: "pub", files: ["pubspec.yaml", "pubspec.lock"], lockfiles: ["pubspec.lock"] },
|
|
12
|
+
{ id: "poetry", files: ["poetry.lock"], lockfiles: ["poetry.lock"] },
|
|
13
|
+
{
|
|
14
|
+
id: "pip",
|
|
15
|
+
files: ["requirements.txt", "Pipfile", "Pipfile.lock"],
|
|
16
|
+
lockfiles: ["Pipfile.lock"],
|
|
17
|
+
},
|
|
18
|
+
{ id: "cargo", files: ["Cargo.lock", "Cargo.toml"], lockfiles: ["Cargo.lock"] },
|
|
19
|
+
{
|
|
20
|
+
id: "gradle",
|
|
21
|
+
files: ["gradlew", "build.gradle", "build.gradle.kts", "settings.gradle"],
|
|
22
|
+
lockfiles: [],
|
|
23
|
+
},
|
|
11
24
|
];
|
|
25
|
+
const NODE_IDS = new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
26
|
+
function basename(relativePath) {
|
|
27
|
+
const parts = relativePath.split("/");
|
|
28
|
+
return (parts[parts.length - 1] ?? relativePath).toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function isRootPath(relativePath) {
|
|
31
|
+
return !relativePath.includes("/");
|
|
32
|
+
}
|
|
12
33
|
/**
|
|
13
34
|
* Detect package managers from lockfiles and manifests.
|
|
14
|
-
*
|
|
35
|
+
* Primary selection prefers root-level evidence and avoids inventing certainty for mixed ecosystems.
|
|
36
|
+
* Poetry requires poetry.lock and/or [tool.poetry] — not bare pyproject.toml.
|
|
15
37
|
*/
|
|
16
38
|
export function detectPackageManagers(input) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}));
|
|
39
|
+
const paths = input.relativePaths;
|
|
40
|
+
const basenames = new Set(paths.map(basename));
|
|
41
|
+
const rootBasenames = new Set(paths.filter(isRootPath).map(basename));
|
|
21
42
|
const found = [];
|
|
43
|
+
const rootFound = [];
|
|
44
|
+
const lockFound = [];
|
|
45
|
+
const rootLockFound = [];
|
|
22
46
|
for (const marker of MARKERS) {
|
|
23
|
-
if (marker.
|
|
24
|
-
|
|
47
|
+
if (marker.id === "npm") {
|
|
48
|
+
const hasNpmLock = marker.lockfiles.some((f) => basenames.has(f.toLowerCase()));
|
|
49
|
+
const hasRootPackage = rootBasenames.has("package.json");
|
|
50
|
+
const hasAnyPackage = paths.some((p) => basename(p) === "package.json");
|
|
51
|
+
if (!hasNpmLock && !hasAnyPackage) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
found.push("npm");
|
|
55
|
+
if (hasRootPackage || hasNpmLock) {
|
|
56
|
+
if (hasRootPackage) {
|
|
57
|
+
rootFound.push("npm");
|
|
58
|
+
}
|
|
59
|
+
if (hasNpmLock) {
|
|
60
|
+
lockFound.push("npm");
|
|
61
|
+
}
|
|
62
|
+
if (marker.lockfiles.some((f) => rootBasenames.has(f.toLowerCase()))) {
|
|
63
|
+
rootLockFound.push("npm");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (marker.id === "poetry") {
|
|
69
|
+
const hasLock = basenames.has("poetry.lock");
|
|
70
|
+
const hasTool = input.poetryToolDetected === true;
|
|
71
|
+
if (!hasLock && !hasTool) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
found.push("poetry");
|
|
75
|
+
if (hasLock || (hasTool && rootBasenames.has("pyproject.toml"))) {
|
|
76
|
+
rootFound.push("poetry");
|
|
77
|
+
}
|
|
78
|
+
if (hasLock) {
|
|
79
|
+
lockFound.push("poetry");
|
|
80
|
+
if (rootBasenames.has("poetry.lock")) {
|
|
81
|
+
rootLockFound.push("poetry");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (marker.id === "pip") {
|
|
87
|
+
const hasMarker = marker.files.some((f) => basenames.has(f.toLowerCase()));
|
|
88
|
+
const hasGenericPyproject = input.genericPyprojectDetected === true;
|
|
89
|
+
if (!hasMarker && !hasGenericPyproject) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
found.push("pip");
|
|
93
|
+
if (marker.files.some((f) => rootBasenames.has(f.toLowerCase())) ||
|
|
94
|
+
(hasGenericPyproject && rootBasenames.has("pyproject.toml"))) {
|
|
95
|
+
rootFound.push("pip");
|
|
96
|
+
}
|
|
97
|
+
if (marker.lockfiles.some((f) => basenames.has(f.toLowerCase()))) {
|
|
98
|
+
lockFound.push("pip");
|
|
99
|
+
}
|
|
100
|
+
if (marker.lockfiles.some((f) => rootBasenames.has(f.toLowerCase()))) {
|
|
101
|
+
rootLockFound.push("pip");
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const hasAny = marker.files.some((f) => basenames.has(f.toLowerCase()));
|
|
106
|
+
if (!hasAny) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
found.push(marker.id);
|
|
110
|
+
if (marker.files.some((f) => rootBasenames.has(f.toLowerCase()))) {
|
|
111
|
+
rootFound.push(marker.id);
|
|
112
|
+
}
|
|
113
|
+
if (marker.lockfiles.some((f) => basenames.has(f.toLowerCase()))) {
|
|
114
|
+
lockFound.push(marker.id);
|
|
115
|
+
}
|
|
116
|
+
if (marker.lockfiles.some((f) => rootBasenames.has(f.toLowerCase()))) {
|
|
117
|
+
rootLockFound.push(marker.id);
|
|
25
118
|
}
|
|
26
|
-
}
|
|
27
|
-
if (basenames.has("package.json") &&
|
|
28
|
-
!found.some((id) => ["npm", "pnpm", "yarn", "bun"].includes(id))) {
|
|
29
|
-
found.push("npm");
|
|
30
119
|
}
|
|
31
120
|
const unique = [...new Set(found)];
|
|
32
121
|
const lockPriority = [
|
|
@@ -34,19 +123,43 @@ export function detectPackageManagers(input) {
|
|
|
34
123
|
"yarn",
|
|
35
124
|
"bun",
|
|
36
125
|
"npm",
|
|
37
|
-
"poetry",
|
|
38
126
|
"composer",
|
|
127
|
+
"pub",
|
|
128
|
+
"poetry",
|
|
39
129
|
"cargo",
|
|
40
130
|
"gradle",
|
|
41
131
|
"pip",
|
|
42
132
|
];
|
|
43
133
|
let primary = "unknown";
|
|
44
134
|
for (const id of lockPriority) {
|
|
45
|
-
if (
|
|
135
|
+
if (rootLockFound.includes(id)) {
|
|
46
136
|
primary = id;
|
|
47
137
|
break;
|
|
48
138
|
}
|
|
49
139
|
}
|
|
140
|
+
if (primary === "unknown") {
|
|
141
|
+
for (const id of lockPriority) {
|
|
142
|
+
if (rootFound.includes(id)) {
|
|
143
|
+
primary = id;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (primary === "unknown") {
|
|
149
|
+
const ecosystems = new Set(unique.map((id) => (NODE_IDS.has(id) ? "node" : id === "unknown" ? "unknown" : id)));
|
|
150
|
+
ecosystems.delete("unknown");
|
|
151
|
+
if (ecosystems.size > 1) {
|
|
152
|
+
primary = "unknown";
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
for (const id of lockPriority) {
|
|
156
|
+
if (lockFound.includes(id) || unique.includes(id)) {
|
|
157
|
+
primary = id;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
50
163
|
return {
|
|
51
164
|
packageManagers: unique.length > 0 ? unique : ["unknown"],
|
|
52
165
|
primaryPackageManager: primary,
|
|
@@ -64,6 +177,8 @@ export function formatPackageManager(id) {
|
|
|
64
177
|
return "Bun";
|
|
65
178
|
case "composer":
|
|
66
179
|
return "Composer";
|
|
180
|
+
case "pub":
|
|
181
|
+
return "Pub";
|
|
67
182
|
case "pip":
|
|
68
183
|
return "pip";
|
|
69
184
|
case "poetry":
|
|
@@ -76,3 +191,13 @@ export function formatPackageManager(id) {
|
|
|
76
191
|
return "Unknown";
|
|
77
192
|
}
|
|
78
193
|
}
|
|
194
|
+
export function formatPackageManagers(ids) {
|
|
195
|
+
const unique = [...new Set(ids)];
|
|
196
|
+
if (unique.length === 0 || (unique.length === 1 && unique[0] === "unknown")) {
|
|
197
|
+
return "Unknown";
|
|
198
|
+
}
|
|
199
|
+
return unique
|
|
200
|
+
.filter((id) => id !== "unknown")
|
|
201
|
+
.map((id) => formatPackageManager(id))
|
|
202
|
+
.join(", ");
|
|
203
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
|
|
2
|
+
import { DEFAULT_IGNORE_DIRECTORIES, DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
|
|
3
3
|
import { discoverFiles } from "../discovery/files.js";
|
|
4
4
|
import { detectFrameworks } from "./framework.js";
|
|
5
5
|
import { detectLanguages } from "./language.js";
|
|
6
6
|
import { detectMonorepo } from "./monorepo.js";
|
|
7
7
|
import { detectPackageManagers } from "./package-manager.js";
|
|
8
|
-
import {
|
|
8
|
+
import { extractPythonDependencyNames, pyprojectHasPoetryTool } from "./python-deps.js";
|
|
9
|
+
import { isDirectory, readJsonFile, readTextFile } from "../utils/fs.js";
|
|
9
10
|
import { resolveRepoRoot } from "../utils/path.js";
|
|
11
|
+
function basename(relativePath) {
|
|
12
|
+
const parts = relativePath.split("/");
|
|
13
|
+
return parts[parts.length - 1] ?? relativePath;
|
|
14
|
+
}
|
|
10
15
|
/**
|
|
11
16
|
* Discover files and detect repository characteristics.
|
|
12
17
|
*/
|
|
@@ -19,31 +24,86 @@ export async function detectProject(cwd, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZ
|
|
|
19
24
|
const discovery = await discoverFiles({ root, maxFileSizeBytes });
|
|
20
25
|
const relativePaths = discovery.files.map((f) => f.relativePath);
|
|
21
26
|
for (const err of discovery.permissionErrors) {
|
|
27
|
+
const relative = err.split(":")[0] ?? err;
|
|
28
|
+
const underIgnored = relative
|
|
29
|
+
.split("/")
|
|
30
|
+
.some((segment) => segment.length > 0 && DEFAULT_IGNORE_DIRECTORIES.has(segment));
|
|
31
|
+
if (underIgnored) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
22
34
|
diagnostics.push(`Permission/read issue: ${err}`);
|
|
23
35
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
const mergedPackageDeps = {};
|
|
37
|
+
let rootWorkspaces;
|
|
38
|
+
const packageJsonPaths = relativePaths.filter((p) => basename(p).toLowerCase() === "package.json");
|
|
39
|
+
for (const relative of packageJsonPaths) {
|
|
40
|
+
const absolute = path.join(root, relative);
|
|
41
|
+
const packageResult = await readJsonFile(absolute, maxFileSizeBytes);
|
|
42
|
+
if (!packageResult.ok) {
|
|
43
|
+
if (relative === "package.json") {
|
|
44
|
+
diagnostics.push(`Malformed package.json: ${packageResult.error}`);
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
Object.assign(mergedPackageDeps, packageResult.data.dependencies ?? {}, packageResult.data.devDependencies ?? {}, packageResult.data.peerDependencies ?? {});
|
|
49
|
+
if (relative === "package.json") {
|
|
50
|
+
rootWorkspaces = packageResult.data.workspaces;
|
|
51
|
+
}
|
|
29
52
|
}
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
const composerRequire = {};
|
|
54
|
+
const composerPaths = relativePaths.filter((p) => basename(p).toLowerCase() === "composer.json");
|
|
55
|
+
for (const relative of composerPaths) {
|
|
56
|
+
const absolute = path.join(root, relative);
|
|
57
|
+
const result = await readJsonFile(absolute, maxFileSizeBytes);
|
|
58
|
+
if (!result.ok) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
Object.assign(composerRequire, result.data.require ?? {}, result.data["require-dev"] ?? {});
|
|
62
|
+
}
|
|
63
|
+
const pythonDependencyNames = new Set();
|
|
64
|
+
let poetryToolDetected = false;
|
|
65
|
+
let genericPyprojectDetected = false;
|
|
66
|
+
for (const relative of relativePaths) {
|
|
67
|
+
const base = basename(relative).toLowerCase();
|
|
68
|
+
if (base !== "pyproject.toml" &&
|
|
69
|
+
base !== "requirements.txt" &&
|
|
70
|
+
!/^requirements(-[a-z0-9_-]+)?\.txt$/i.test(base)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const text = await readTextFile(path.join(root, relative), maxFileSizeBytes);
|
|
74
|
+
if (!text) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const name of extractPythonDependencyNames(text)) {
|
|
78
|
+
pythonDependencyNames.add(name);
|
|
79
|
+
}
|
|
80
|
+
if (base === "pyproject.toml") {
|
|
81
|
+
if (pyprojectHasPoetryTool(text)) {
|
|
82
|
+
poetryToolDetected = true;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
genericPyprojectDetected = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (relativePaths.some((p) => basename(p).toLowerCase() === "pipfile")) {
|
|
90
|
+
genericPyprojectDetected = true;
|
|
32
91
|
}
|
|
33
|
-
const deps = {
|
|
34
|
-
...(packageJson?.dependencies ?? {}),
|
|
35
|
-
...(packageJson?.devDependencies ?? {}),
|
|
36
|
-
...(packageJson?.peerDependencies ?? {}),
|
|
37
|
-
};
|
|
38
92
|
const languages = detectLanguages({ relativePaths });
|
|
39
93
|
const frameworks = detectFrameworks({
|
|
40
94
|
relativePaths,
|
|
41
|
-
packageJsonDependencies:
|
|
95
|
+
packageJsonDependencies: mergedPackageDeps,
|
|
96
|
+
composerJsonRequire: composerRequire,
|
|
97
|
+
pythonDependencyNames: [...pythonDependencyNames],
|
|
98
|
+
});
|
|
99
|
+
const packageManagers = detectPackageManagers({
|
|
100
|
+
relativePaths,
|
|
101
|
+
poetryToolDetected,
|
|
102
|
+
genericPyprojectDetected,
|
|
42
103
|
});
|
|
43
|
-
const packageManagers = detectPackageManagers({ relativePaths });
|
|
44
104
|
const monorepo = detectMonorepo({
|
|
45
105
|
relativePaths,
|
|
46
|
-
packageJsonWorkspaces:
|
|
106
|
+
packageJsonWorkspaces: rootWorkspaces,
|
|
47
107
|
});
|
|
48
108
|
const repository = {
|
|
49
109
|
root,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Python dependency name extraction from manifests.
|
|
3
|
+
* Filename/table semantics only — does not evaluate environments.
|
|
4
|
+
*/
|
|
5
|
+
export declare function pyprojectHasPoetryTool(text: string): boolean;
|
|
6
|
+
export declare function extractPythonDependencyNames(text: string): string[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Python dependency name extraction from manifests.
|
|
3
|
+
* Filename/table semantics only — does not evaluate environments.
|
|
4
|
+
*/
|
|
5
|
+
const SKIP_KEYS = new Set([
|
|
6
|
+
"python",
|
|
7
|
+
"package-mode",
|
|
8
|
+
"packages",
|
|
9
|
+
"include",
|
|
10
|
+
"exclude",
|
|
11
|
+
"readme",
|
|
12
|
+
"homepage",
|
|
13
|
+
"repository",
|
|
14
|
+
"documentation",
|
|
15
|
+
"keywords",
|
|
16
|
+
"classifiers",
|
|
17
|
+
"license",
|
|
18
|
+
"authors",
|
|
19
|
+
"maintainers",
|
|
20
|
+
"dependencies",
|
|
21
|
+
"dev-dependencies",
|
|
22
|
+
"group",
|
|
23
|
+
]);
|
|
24
|
+
export function pyprojectHasPoetryTool(text) {
|
|
25
|
+
return /\[tool\.poetry(?:\.|\])/m.test(text);
|
|
26
|
+
}
|
|
27
|
+
export function extractPythonDependencyNames(text) {
|
|
28
|
+
const names = new Set();
|
|
29
|
+
for (const match of text.matchAll(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*["'{[]/gm)) {
|
|
30
|
+
const name = (match[1] ?? "").toLowerCase().replace(/_/g, "-");
|
|
31
|
+
if (name && !SKIP_KEYS.has(name) && !name.startsWith("tool.")) {
|
|
32
|
+
names.add(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const match of text.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?(?:\s*[><=! ~^,;]|["'])/g)) {
|
|
36
|
+
const name = (match[1] ?? "").toLowerCase().replace(/_/g, "-");
|
|
37
|
+
if (name && !SKIP_KEYS.has(name)) {
|
|
38
|
+
names.add(name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
42
|
+
const line = rawLine.trim();
|
|
43
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const name = line
|
|
47
|
+
.split(/[><=![;@\s]/)[0]
|
|
48
|
+
?.toLowerCase()
|
|
49
|
+
.replace(/_/g, "-");
|
|
50
|
+
if (name && /^[a-z0-9][a-z0-9._-]*$/.test(name) && !SKIP_KEYS.has(name)) {
|
|
51
|
+
names.add(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...names];
|
|
55
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { scan } from "./core/scanner/scan.js";
|
|
2
2
|
export { PACKAGE_VERSION } from "./constants.js";
|
|
3
|
-
export type { ScanOptions, ScanResult, RepositoryInfo, Finding, FindingsSummary, Scores, AgentPresence, AgentDetectionResult, CliOptions, ExitCode, } from "./types/index.js";
|
|
3
|
+
export type { ScanOptions, ScanResult, RepositoryInfo, Finding, FindingsSummary, Scores, AgentPresence, AgentDetectionResult, AgentSecurityAnalysisMode, CliOptions, ExitCode, } from "./types/index.js";
|
|
4
4
|
export { EXIT_CODES } from "./types/index.js";
|
|
5
5
|
export { agentRegistry } from "./agents/registry.js";
|
|
6
6
|
export { detectAgents } from "./agents/detect-agents.js";
|
|
@@ -74,6 +74,7 @@ export function renderJsonReport(result) {
|
|
|
74
74
|
summary: result.summary,
|
|
75
75
|
scoringAvailable: result.scoringAvailable,
|
|
76
76
|
scores: result.scores,
|
|
77
|
+
agentSecurityAnalysis: result.agentSecurityAnalysis,
|
|
77
78
|
timing: result.timing,
|
|
78
79
|
diagnostics: {
|
|
79
80
|
warnings: result.diagnostics.warnings.map(sanitizeForOutput),
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { PACKAGE_VERSION } from "../../constants.js";
|
|
2
2
|
import { AGENT_DISPLAY_NAMES } from "../../constants.js";
|
|
3
|
-
import { formatFramework } from "../../detectors/framework.js";
|
|
4
|
-
import { formatLanguage } from "../../detectors/language.js";
|
|
3
|
+
import { formatFramework, formatFrameworks } from "../../detectors/framework.js";
|
|
4
|
+
import { formatLanguage, formatLanguages } from "../../detectors/language.js";
|
|
5
5
|
import { formatMonorepo } from "../../detectors/monorepo.js";
|
|
6
|
-
import { formatPackageManager } from "../../detectors/package-manager.js";
|
|
6
|
+
import { formatPackageManager, formatPackageManagers } from "../../detectors/package-manager.js";
|
|
7
7
|
import { colors, symbolFail, symbolInfo, symbolOk, symbolWarn } from "../../utils/colors.js";
|
|
8
8
|
import { sanitizeTerminalText } from "../../security/redaction.js";
|
|
9
9
|
const DEFAULT_FINDING_LIMIT = 12;
|
|
10
|
+
const LIMITED_ANALYSIS_NOTE = "No supported coding-agent configuration detected; agent-specific security exposure checks are limited.";
|
|
10
11
|
function groupFindings(findings) {
|
|
11
12
|
return {
|
|
12
13
|
critical: findings.filter((f) => f.severity === "critical"),
|
|
@@ -26,6 +27,14 @@ function agentGlyph(configured, detected) {
|
|
|
26
27
|
function formatAgents(agents) {
|
|
27
28
|
return agents.map((id) => AGENT_DISPLAY_NAMES[id] ?? id).join(", ");
|
|
28
29
|
}
|
|
30
|
+
function isMultiStack(result) {
|
|
31
|
+
if (result.repository.monorepo === "multi-project") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const frameworks = result.repository.frameworks.filter((id) => id !== "unknown");
|
|
35
|
+
const managers = result.repository.packageManagers.filter((id) => id !== "unknown");
|
|
36
|
+
return frameworks.length > 1 || managers.length > 1;
|
|
37
|
+
}
|
|
29
38
|
function renderFindingBlock(finding, verbose) {
|
|
30
39
|
const lines = [];
|
|
31
40
|
const icon = finding.severity === "critical"
|
|
@@ -62,9 +71,20 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
62
71
|
lines.push(colors.dim("Scanning repository..."));
|
|
63
72
|
lines.push("");
|
|
64
73
|
lines.push(colors.bold("Repository"));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
if (isMultiStack(result)) {
|
|
75
|
+
lines.push(` Languages: ${sanitizeTerminalText(formatLanguages(result.repository.languages))}`);
|
|
76
|
+
lines.push(` Frameworks: ${sanitizeTerminalText(formatFrameworks(result.repository.frameworks))}`);
|
|
77
|
+
lines.push(` Package managers: ${sanitizeTerminalText(formatPackageManagers(result.repository.packageManagers))}`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
lines.push(` Framework: ${sanitizeTerminalText(formatFramework(result.repository.primaryFramework))}`);
|
|
81
|
+
lines.push(` Language: ${sanitizeTerminalText(formatLanguage(result.repository.primaryLanguage))}`);
|
|
82
|
+
const packageManagerLabel = result.repository.packageManagers.length > 1 ||
|
|
83
|
+
result.repository.primaryPackageManager === "unknown"
|
|
84
|
+
? formatPackageManagers(result.repository.packageManagers)
|
|
85
|
+
: formatPackageManager(result.repository.primaryPackageManager);
|
|
86
|
+
lines.push(` Package manager: ${sanitizeTerminalText(packageManagerLabel)}`);
|
|
87
|
+
}
|
|
68
88
|
if (result.repository.monorepo !== "none") {
|
|
69
89
|
lines.push(` Monorepo: ${sanitizeTerminalText(formatMonorepo(result.repository.monorepo))}`);
|
|
70
90
|
}
|
|
@@ -86,10 +106,20 @@ export function renderTerminalReport(result, options = {}) {
|
|
|
86
106
|
lines.push("");
|
|
87
107
|
const grouped = groupFindings(result.findings);
|
|
88
108
|
const total = result.summary.total;
|
|
109
|
+
const limited = result.agentSecurityAnalysis === "limited";
|
|
89
110
|
lines.push(colors.bold("Findings"));
|
|
90
111
|
lines.push("");
|
|
112
|
+
if (limited) {
|
|
113
|
+
lines.push(` ${symbolWarn()} ${sanitizeTerminalText(LIMITED_ANALYSIS_NOTE)}`);
|
|
114
|
+
lines.push("");
|
|
115
|
+
}
|
|
91
116
|
if (total === 0) {
|
|
92
|
-
|
|
117
|
+
if (limited) {
|
|
118
|
+
lines.push(` ${symbolOk()} No agent-configuration findings`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
lines.push(` ${symbolOk()} No findings`);
|
|
122
|
+
}
|
|
93
123
|
lines.push("");
|
|
94
124
|
}
|
|
95
125
|
else {
|