agentsafe-cli 0.1.0
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/README.md +36 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +20 -0
- package/dist/config.js.map +1 -0
- package/dist/domain.d.ts +1 -0
- package/dist/domain.js +23 -0
- package/dist/domain.js.map +1 -0
- package/dist/files.d.ts +7 -0
- package/dist/files.js +66 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.js +42 -0
- package/dist/prompts.js.map +1 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.js +74 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +2 -0
- package/dist/rules.js +20 -0
- package/dist/rules.js.map +1 -0
- package/dist/scanner.d.ts +8 -0
- package/dist/scanner.js +137 -0
- package/dist/scanner.js.map +1 -0
- package/dist/structured.d.ts +6 -0
- package/dist/structured.js +71 -0
- package/dist/structured.js.map +1 -0
- package/dist/target.d.ts +3 -0
- package/dist/target.js +39 -0
- package/dist/target.js.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
- package/rules/default-rules.yaml +161 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# AgentSafe
|
|
2
|
+
|
|
3
|
+
AgentSafe 是一个面向 AI 编程 Agent 使用场景的开源项目安装前安全体检 CLI。它只做静态扫描,不执行目标项目代码,默认在本地输出中文 Markdown 和 JSON 风险报告。
|
|
4
|
+
|
|
5
|
+
## 开发运行
|
|
6
|
+
|
|
7
|
+
```powershell
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm test
|
|
10
|
+
pnpm build
|
|
11
|
+
pnpm dev -- scan . -o agentsafe-report
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
如果当前终端没有 Node/pnpm,可以在 Codex 桌面环境中使用自带运行时:
|
|
15
|
+
|
|
16
|
+
```powershell
|
|
17
|
+
& 'C:\Users\95365\.cache\codex-runtimes\codex-primary-runtime\dependencies\bin\pnpm.cmd' install
|
|
18
|
+
& 'C:\Users\95365\.cache\codex-runtimes\codex-primary-runtime\dependencies\bin\pnpm.cmd' test
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## CLI
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
agentsafe scan <本地目录或 Git URL> -o agentsafe-report
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
常用参数:
|
|
28
|
+
|
|
29
|
+
- `-o, --out-dir <dir>`:报告输出目录,默认 `agentsafe-report`
|
|
30
|
+
- `--allow-domain <domain...>`:把公司内网、私有 registry 等域名加入网络风险白名单
|
|
31
|
+
- `--git-binary <path>`:指定 Git 可执行文件路径
|
|
32
|
+
|
|
33
|
+
输出文件:
|
|
34
|
+
|
|
35
|
+
- `report.md`:中文风险报告
|
|
36
|
+
- `report.json`:结构化扫描结果
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const projectRoot: string;
|
|
2
|
+
export declare const defaultRulesPath: string;
|
|
3
|
+
export declare const defaultReportDir = "agentsafe-report";
|
|
4
|
+
export declare const severityRank: {
|
|
5
|
+
readonly low: 1;
|
|
6
|
+
readonly medium: 2;
|
|
7
|
+
readonly high: 3;
|
|
8
|
+
readonly block: 4;
|
|
9
|
+
};
|
|
10
|
+
export declare const severityWeight: {
|
|
11
|
+
readonly low: 10;
|
|
12
|
+
readonly medium: 25;
|
|
13
|
+
readonly high: 55;
|
|
14
|
+
readonly block: 100;
|
|
15
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
4
|
+
const srcDir = path.dirname(currentFile);
|
|
5
|
+
export const projectRoot = path.resolve(srcDir, "..");
|
|
6
|
+
export const defaultRulesPath = path.join(projectRoot, "rules", "default-rules.yaml");
|
|
7
|
+
export const defaultReportDir = "agentsafe-report";
|
|
8
|
+
export const severityRank = {
|
|
9
|
+
low: 1,
|
|
10
|
+
medium: 2,
|
|
11
|
+
high: 3,
|
|
12
|
+
block: 4
|
|
13
|
+
};
|
|
14
|
+
export const severityWeight = {
|
|
15
|
+
low: 10,
|
|
16
|
+
medium: 25,
|
|
17
|
+
high: 55,
|
|
18
|
+
block: 100
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;AAEzC,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AACtD,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;AACtF,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,GAAG,EAAE,CAAC;IACN,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACA,CAAC;AAEX,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,GAAG,EAAE,EAAE;IACP,MAAM,EAAE,EAAE;IACV,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,GAAG;CACF,CAAC"}
|
package/dist/domain.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function allMatchedDomainsAllowed(snippet: string, allowDomains: string[]): boolean;
|
package/dist/domain.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function allMatchedDomainsAllowed(snippet, allowDomains) {
|
|
2
|
+
const urls = extractUrls(snippet);
|
|
3
|
+
if (!urls.length || !allowDomains.length) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
return urls.every((url) => {
|
|
7
|
+
try {
|
|
8
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
9
|
+
return allowDomains.some((domain) => domainMatches(hostname, domain));
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function extractUrls(text) {
|
|
17
|
+
return text.match(/https?:\/\/[^\s"'`<>)]*/gi) ?? [];
|
|
18
|
+
}
|
|
19
|
+
function domainMatches(hostname, allowed) {
|
|
20
|
+
const normalized = allowed.toLowerCase().replace(/^\*\./, "");
|
|
21
|
+
return hostname === normalized || hostname.endsWith(`.${normalized}`);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain.js","sourceRoot":"","sources":["../src/domain.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,wBAAwB,CAAC,OAAe,EAAE,YAAsB;IAC9E,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IAElC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACxB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;YACrD,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,2BAA2B,CAAC,IAAI,EAAE,CAAC;AACvD,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB,EAAE,OAAe;IACtD,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9D,OAAO,QAAQ,KAAK,UAAU,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,UAAU,EAAE,CAAC,CAAC;AACxE,CAAC"}
|
package/dist/files.d.ts
ADDED
package/dist/files.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
const scanPatterns = [
|
|
5
|
+
"**/README",
|
|
6
|
+
"**/README.md",
|
|
7
|
+
"**/README.MD",
|
|
8
|
+
"**/package.json",
|
|
9
|
+
"**/package-lock.json",
|
|
10
|
+
"**/pnpm-lock.yaml",
|
|
11
|
+
"**/yarn.lock",
|
|
12
|
+
"**/requirements*.txt",
|
|
13
|
+
"**/pyproject.toml",
|
|
14
|
+
"**/setup.py",
|
|
15
|
+
"**/Dockerfile",
|
|
16
|
+
"**/Dockerfile.*",
|
|
17
|
+
"**/.github/workflows/*.yml",
|
|
18
|
+
"**/.github/workflows/*.yaml",
|
|
19
|
+
"**/.env.example",
|
|
20
|
+
"**/*.sh",
|
|
21
|
+
"**/*.bash",
|
|
22
|
+
"**/*.zsh",
|
|
23
|
+
"**/*.ps1"
|
|
24
|
+
];
|
|
25
|
+
const ignorePatterns = [
|
|
26
|
+
"**/.git/**",
|
|
27
|
+
"**/node_modules/**",
|
|
28
|
+
"**/dist/**",
|
|
29
|
+
"**/build/**",
|
|
30
|
+
"**/coverage/**",
|
|
31
|
+
"**/.next/**",
|
|
32
|
+
"**/.turbo/**",
|
|
33
|
+
"**/.venv/**",
|
|
34
|
+
"**/venv/**",
|
|
35
|
+
"**/__pycache__/**"
|
|
36
|
+
];
|
|
37
|
+
const maxFileBytes = 1024 * 1024;
|
|
38
|
+
export async function collectFiles(rootDir) {
|
|
39
|
+
const entries = await fg(scanPatterns, {
|
|
40
|
+
cwd: rootDir,
|
|
41
|
+
dot: true,
|
|
42
|
+
onlyFiles: true,
|
|
43
|
+
unique: true,
|
|
44
|
+
ignore: ignorePatterns
|
|
45
|
+
});
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const relativePath of entries.sort()) {
|
|
48
|
+
const absolutePath = path.join(rootDir, relativePath);
|
|
49
|
+
const stat = await fs.stat(absolutePath);
|
|
50
|
+
if (stat.size > maxFileBytes) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const content = await fs.readFile(absolutePath, "utf8").catch(() => "");
|
|
54
|
+
files.push({
|
|
55
|
+
absolutePath,
|
|
56
|
+
relativePath: normalizePath(relativePath),
|
|
57
|
+
content,
|
|
58
|
+
bytes: stat.size
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
function normalizePath(filePath) {
|
|
64
|
+
return filePath.split(path.sep).join("/");
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,WAAW,CAAC;AAS3B,MAAM,YAAY,GAAG;IACnB,WAAW;IACX,cAAc;IACd,cAAc;IACd,iBAAiB;IACjB,sBAAsB;IACtB,mBAAmB;IACnB,cAAc;IACd,sBAAsB;IACtB,mBAAmB;IACnB,aAAa;IACb,eAAe;IACf,iBAAiB;IACjB,4BAA4B;IAC5B,6BAA6B;IAC7B,iBAAiB;IACjB,SAAS;IACT,WAAW;IACX,UAAU;IACV,UAAU;CACX,CAAC;AAEF,MAAM,cAAc,GAAG;IACrB,YAAY;IACZ,oBAAoB;IACpB,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,aAAa;IACb,YAAY;IACZ,mBAAmB;CACpB,CAAC;AAEF,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjC,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,OAAe;IAChD,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,YAAY,EAAE;QACrC,GAAG,EAAE,OAAO;QACZ,GAAG,EAAE,IAAI;QACT,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,cAAc;KACvB,CAAC,CAAC;IAEH,MAAM,KAAK,GAAoB,EAAE,CAAC;IAElC,KAAK,MAAM,YAAY,IAAI,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;QAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAEzC,IAAI,IAAI,CAAC,IAAI,GAAG,YAAY,EAAE,CAAC;YAC7B,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QACxE,KAAK,CAAC,IAAI,CAAC;YACT,YAAY;YACZ,YAAY,EAAE,aAAa,CAAC,YAAY,CAAC;YACzC,OAAO;YACP,KAAK,EAAE,IAAI,CAAC,IAAI;SACjB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB;IACrC,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5C,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { defaultReportDir, defaultRulesPath } from "./config.js";
|
|
5
|
+
import { writeReports } from "./report.js";
|
|
6
|
+
import { loadRules } from "./rules.js";
|
|
7
|
+
import { runScan } from "./scanner.js";
|
|
8
|
+
import { resolveTarget } from "./target.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name("agentsafe")
|
|
12
|
+
.description("AI Agent 开源项目安装前安全体检工具")
|
|
13
|
+
.version("0.1.0");
|
|
14
|
+
program
|
|
15
|
+
.command("scan")
|
|
16
|
+
.argument("<target>", "本地目录或 Git 仓库 URL")
|
|
17
|
+
.option("-o, --out-dir <dir>", "报告输出目录", defaultReportDir)
|
|
18
|
+
.option("--allow-domain <domain...>", "允许的可信域名,可传多个")
|
|
19
|
+
.option("--git-binary <path>", "Git 可执行文件路径")
|
|
20
|
+
.action(async (target, options) => {
|
|
21
|
+
let cleanup;
|
|
22
|
+
try {
|
|
23
|
+
const resolved = await resolveTarget(target, options.gitBinary);
|
|
24
|
+
cleanup = resolved.cleanup;
|
|
25
|
+
const rules = await loadRules(defaultRulesPath);
|
|
26
|
+
const report = await runScan({
|
|
27
|
+
target: resolved.original,
|
|
28
|
+
rootDir: resolved.rootDir,
|
|
29
|
+
rules,
|
|
30
|
+
allowDomains: options.allowDomain ?? []
|
|
31
|
+
});
|
|
32
|
+
const outDir = path.resolve(options.outDir);
|
|
33
|
+
const paths = await writeReports(report, outDir);
|
|
34
|
+
console.log(`AgentSafe 扫描完成:${report.summary.totalFindings} 个风险项,风险分 ${report.summary.score}/100`);
|
|
35
|
+
console.log(`Markdown 报告:${paths.markdownPath}`);
|
|
36
|
+
console.log(`JSON 报告:${paths.jsonPath}`);
|
|
37
|
+
if (report.summary.bySeverity.block > 0) {
|
|
38
|
+
process.exitCode = 2;
|
|
39
|
+
}
|
|
40
|
+
else if (report.summary.bySeverity.high > 0) {
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
console.error(`AgentSafe 扫描失败:${message}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
await cleanup?.();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
program.parseAsync(process.argv);
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,wBAAwB,CAAC;KACrC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;KACxC,MAAM,CAAC,qBAAqB,EAAE,QAAQ,EAAE,gBAAgB,CAAC;KACzD,MAAM,CAAC,4BAA4B,EAAE,cAAc,CAAC;KACpD,MAAM,CAAC,qBAAqB,EAAE,aAAa,CAAC;KAC5C,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,OAI9B,EAAE,EAAE;IACH,IAAI,OAA0C,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAChE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;QAE3B,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,gBAAgB,CAAC,CAAC;QAChD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;YAC3B,MAAM,EAAE,QAAQ,CAAC,QAAQ;YACzB,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,KAAK;YACL,YAAY,EAAE,OAAO,CAAC,WAAW,IAAI,EAAE;SACxC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEjD,OAAO,CAAC,GAAG,CAAC,kBAAkB,MAAM,CAAC,OAAO,CAAC,aAAa,aAAa,MAAM,CAAC,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;QACnG,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAEzC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;YACxC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;aAAM,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC9C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,kBAAkB,OAAO,EAAE,CAAC,CAAC;QAC3C,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,EAAE,EAAE,CAAC;IACpB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function generateRecommendations(findings) {
|
|
2
|
+
const recommendations = new Set();
|
|
3
|
+
if (findings.some((finding) => finding.severity === "block")) {
|
|
4
|
+
recommendations.add("存在阻断级风险:不要让 AI Agent 自动执行安装、初始化或修复命令。");
|
|
5
|
+
}
|
|
6
|
+
if (findings.some((finding) => finding.category === "package-lifecycle")) {
|
|
7
|
+
recommendations.add("Node 项目建议先使用 npm/pnpm/yarn 的 --ignore-scripts 安装依赖。");
|
|
8
|
+
}
|
|
9
|
+
if (findings.some((finding) => finding.category === "network-exfiltration")) {
|
|
10
|
+
recommendations.add("发现潜在外联上传行为:确认域名可信前不要提供真实密钥和源码目录。");
|
|
11
|
+
}
|
|
12
|
+
if (findings.some((finding) => finding.category === "credential-access")) {
|
|
13
|
+
recommendations.add("发现凭据读取迹象:请在无真实 SSH、云厂商、浏览器登录态的沙箱账户中复核。");
|
|
14
|
+
}
|
|
15
|
+
if (!findings.length) {
|
|
16
|
+
recommendations.add("未发现内置规则命中的高风险模式,但仍建议分步运行安装命令并保留人工确认。");
|
|
17
|
+
}
|
|
18
|
+
recommendations.add("给 Agent 的执行权限应保持最小化:禁止自动运行 curl|bash、postinstall、未知脚本和需要 sudo 的命令。");
|
|
19
|
+
return [...recommendations];
|
|
20
|
+
}
|
|
21
|
+
export function generateAgentPrompt(findings) {
|
|
22
|
+
const blocked = findings.filter((finding) => finding.severity === "block");
|
|
23
|
+
const high = findings.filter((finding) => finding.severity === "high");
|
|
24
|
+
const riskyItems = [...blocked, ...high].slice(0, 8);
|
|
25
|
+
const riskyText = riskyItems.length
|
|
26
|
+
? riskyItems
|
|
27
|
+
.map((finding) => `- ${finding.filePath}:${finding.line} 命中 ${finding.title},不要自动运行相关命令。`)
|
|
28
|
+
.join("\n")
|
|
29
|
+
: "- 暂未发现阻断/高风险命中,但仍需逐条解释命令后再请求用户确认。";
|
|
30
|
+
return [
|
|
31
|
+
"你是 AI 编程 Agent。接下来处理该项目时必须遵守:",
|
|
32
|
+
"",
|
|
33
|
+
"1. 不要自动执行安装、初始化、修复、postinstall、prepare、curl|bash、wget|sh、sudo、chmod 777、未知 shell 脚本。",
|
|
34
|
+
"2. 运行任何命令前,先用中文解释命令目的、会读取/写入哪些路径、是否会联网。",
|
|
35
|
+
"3. 如需安装依赖,优先使用忽略生命周期脚本的方式,并等待用户确认。",
|
|
36
|
+
"4. 如命令会访问 SSH、云厂商凭据、.env、浏览器目录或上传文件,必须停止并请求用户确认。",
|
|
37
|
+
"",
|
|
38
|
+
"本次扫描需特别注意:",
|
|
39
|
+
riskyText
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=prompts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,uBAAuB,CAAC,QAAmB;IACzD,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,EAAE,CAAC;QAC7D,eAAe,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IAC/D,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,mBAAmB,CAAC,EAAE,CAAC;QACzE,eAAe,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,sBAAsB,CAAC,EAAE,CAAC;QAC5E,eAAe,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,mBAAmB,CAAC,EAAE,CAAC;QACzE,eAAe,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrB,eAAe,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;IAC9D,CAAC;IAED,eAAe,CAAC,GAAG,CAAC,oEAAoE,CAAC,CAAC;IAE1F,OAAO,CAAC,GAAG,eAAe,CAAC,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAmB;IACrD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC;IACvE,MAAM,UAAU,GAAG,CAAC,GAAG,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM;QACjC,CAAC,CAAC,UAAU;aACT,GAAG,CACF,CAAC,OAAO,EAAE,EAAE,CACV,KAAK,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,OAAO,OAAO,CAAC,KAAK,cAAc,CAC1E;aACA,IAAI,CAAC,IAAI,CAAC;QACb,CAAC,CAAC,mCAAmC,CAAC;IAExC,OAAO;QACL,+BAA+B;QAC/B,EAAE;QACF,sFAAsF;QACtF,yCAAyC;QACzC,oCAAoC;QACpC,kDAAkD;QAClD,EAAE;QACF,YAAY;QACZ,SAAS;KACV,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
package/dist/report.d.ts
ADDED
package/dist/report.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const severityZh = {
|
|
4
|
+
low: "低",
|
|
5
|
+
medium: "中",
|
|
6
|
+
high: "高",
|
|
7
|
+
block: "阻断"
|
|
8
|
+
};
|
|
9
|
+
export async function writeReports(report, outDir) {
|
|
10
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
11
|
+
const markdownPath = path.join(outDir, "report.md");
|
|
12
|
+
const jsonPath = path.join(outDir, "report.json");
|
|
13
|
+
await fs.writeFile(markdownPath, renderMarkdownReport(report), "utf8");
|
|
14
|
+
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
15
|
+
return {
|
|
16
|
+
markdownPath,
|
|
17
|
+
jsonPath
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function renderMarkdownReport(report) {
|
|
21
|
+
const lines = [
|
|
22
|
+
"# AgentSafe 风险报告",
|
|
23
|
+
"",
|
|
24
|
+
`- 扫描目标:${report.target}`,
|
|
25
|
+
`- 扫描路径:${report.rootDir}`,
|
|
26
|
+
`- 扫描时间:${report.scannedAt}`,
|
|
27
|
+
`- 风险分:${report.summary.score}/100`,
|
|
28
|
+
`- 扫描文件数:${report.summary.scannedFiles}`,
|
|
29
|
+
`- 风险项总数:${report.summary.totalFindings}`,
|
|
30
|
+
"",
|
|
31
|
+
"## 风险等级统计",
|
|
32
|
+
"",
|
|
33
|
+
"| 等级 | 数量 |",
|
|
34
|
+
"| --- | ---: |",
|
|
35
|
+
`| 阻断 | ${report.summary.bySeverity.block} |`,
|
|
36
|
+
`| 高 | ${report.summary.bySeverity.high} |`,
|
|
37
|
+
`| 中 | ${report.summary.bySeverity.medium} |`,
|
|
38
|
+
`| 低 | ${report.summary.bySeverity.low} |`,
|
|
39
|
+
"",
|
|
40
|
+
"## 建议",
|
|
41
|
+
"",
|
|
42
|
+
...report.recommendations.map((item) => `- ${item}`),
|
|
43
|
+
"",
|
|
44
|
+
"## 风险明细",
|
|
45
|
+
""
|
|
46
|
+
];
|
|
47
|
+
if (!report.findings.length) {
|
|
48
|
+
lines.push("未发现内置规则命中的风险项。", "");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const finding of report.findings) {
|
|
52
|
+
lines.push(...renderFinding(finding));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
lines.push("## 给 AI 编程 Agent 的安全执行提示", "", "```text", report.agentPrompt, "```", "", "## 已扫描文件", "", "| 文件 | 字节 | 命中数 |", "| --- | ---: | ---: |", ...report.scannedFiles.map((file) => `| ${file.path} | ${file.bytes} | ${file.matchedFindings} |`), "");
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function renderFinding(finding) {
|
|
59
|
+
return [
|
|
60
|
+
`### ${severityZh[finding.severity]}:${finding.title}`,
|
|
61
|
+
"",
|
|
62
|
+
`- 位置:${finding.filePath}:${finding.line}:${finding.column}`,
|
|
63
|
+
`- 规则:${finding.ruleId}`,
|
|
64
|
+
`- 分类:${finding.category}`,
|
|
65
|
+
`- 说明:${finding.explainZh}`,
|
|
66
|
+
`- 建议:${finding.safeFix}`,
|
|
67
|
+
"",
|
|
68
|
+
"```text",
|
|
69
|
+
finding.snippet,
|
|
70
|
+
"```",
|
|
71
|
+
""
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,UAAU,GAA6B;IAC3C,GAAG,EAAE,GAAG;IACR,MAAM,EAAE,GAAG;IACX,IAAI,EAAE,GAAG;IACT,KAAK,EAAE,IAAI;CACZ,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAkB,EAAE,MAAc;IAInE,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAElD,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,oBAAoB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;IACvE,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAE7E,OAAO;QACL,YAAY;QACZ,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAkB;IACrD,MAAM,KAAK,GAAG;QACZ,kBAAkB;QAClB,EAAE;QACF,UAAU,MAAM,CAAC,MAAM,EAAE;QACzB,UAAU,MAAM,CAAC,OAAO,EAAE;QAC1B,UAAU,MAAM,CAAC,SAAS,EAAE;QAC5B,SAAS,MAAM,CAAC,OAAO,CAAC,KAAK,MAAM;QACnC,WAAW,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE;QACxC,WAAW,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE;QACzC,EAAE;QACF,WAAW;QACX,EAAE;QACF,aAAa;QACb,gBAAgB;QAChB,UAAU,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI;QAC7C,SAAS,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,IAAI;QAC3C,SAAS,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,IAAI;QAC7C,SAAS,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,IAAI;QAC1C,EAAE;QACF,OAAO;QACP,EAAE;QACF,GAAG,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,EAAE;QACF,SAAS;QACT,EAAE;KACH,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IACnC,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CACR,0BAA0B,EAC1B,EAAE,EACF,SAAS,EACT,MAAM,CAAC,WAAW,EAClB,KAAK,EACL,EAAE,EACF,UAAU,EACV,EAAE,EACF,mBAAmB,EACnB,uBAAuB,EACvB,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,eAAe,IAAI,CAAC,EAClG,EAAE,CACH,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,aAAa,CAAC,OAAgB;IACrC,OAAO;QACL,OAAO,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE;QACtD,EAAE;QACF,QAAQ,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE;QAC5D,QAAQ,OAAO,CAAC,MAAM,EAAE;QACxB,QAAQ,OAAO,CAAC,QAAQ,EAAE;QAC1B,QAAQ,OAAO,CAAC,SAAS,EAAE;QAC3B,QAAQ,OAAO,CAAC,OAAO,EAAE;QACzB,EAAE;QACF,SAAS;QACT,OAAO,CAAC,OAAO;QACf,KAAK;QACL,EAAE;KACH,CAAC;AACJ,CAAC"}
|
package/dist/rules.d.ts
ADDED
package/dist/rules.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import YAML from "yaml";
|
|
3
|
+
export async function loadRules(ruleFile) {
|
|
4
|
+
const content = await fs.readFile(ruleFile, "utf8");
|
|
5
|
+
const parsed = YAML.parse(content);
|
|
6
|
+
if (!parsed?.rules?.length) {
|
|
7
|
+
throw new Error(`规则文件为空或格式不正确: ${ruleFile}`);
|
|
8
|
+
}
|
|
9
|
+
return parsed.rules.map((rule) => ({
|
|
10
|
+
id: rule.id,
|
|
11
|
+
title: rule.title,
|
|
12
|
+
severity: rule.severity,
|
|
13
|
+
category: rule.category,
|
|
14
|
+
patterns: rule.patterns,
|
|
15
|
+
fileGlobs: rule.file_globs,
|
|
16
|
+
explainZh: rule.explain_zh,
|
|
17
|
+
safeFix: rule.safe_fix
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB;IAC9C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAgB,CAAC;IAElD,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACjC,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,SAAS,EAAE,IAAI,CAAC,UAAU;QAC1B,OAAO,EAAE,IAAI,CAAC,QAAQ;KACvB,CAAC,CAAC,CAAC;AACN,CAAC"}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import picomatch from "picomatch";
|
|
2
|
+
import { severityRank, severityWeight } from "./config.js";
|
|
3
|
+
import { allMatchedDomainsAllowed } from "./domain.js";
|
|
4
|
+
import { collectFiles } from "./files.js";
|
|
5
|
+
import { generateAgentPrompt, generateRecommendations } from "./prompts.js";
|
|
6
|
+
import { scanStructuredFile } from "./structured.js";
|
|
7
|
+
const emptySeverityCounts = {
|
|
8
|
+
low: 0,
|
|
9
|
+
medium: 0,
|
|
10
|
+
high: 0,
|
|
11
|
+
block: 0
|
|
12
|
+
};
|
|
13
|
+
export async function runScan(input) {
|
|
14
|
+
const files = await collectFiles(input.rootDir);
|
|
15
|
+
const findings = [];
|
|
16
|
+
const scannedFiles = [];
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const fileFindings = [
|
|
19
|
+
...scanStructuredFile({
|
|
20
|
+
relativePath: file.relativePath,
|
|
21
|
+
content: file.content,
|
|
22
|
+
rules: input.rules
|
|
23
|
+
}),
|
|
24
|
+
...scanContent({
|
|
25
|
+
relativePath: file.relativePath,
|
|
26
|
+
content: file.content,
|
|
27
|
+
rules: input.rules,
|
|
28
|
+
allowDomains: input.allowDomains ?? []
|
|
29
|
+
})
|
|
30
|
+
];
|
|
31
|
+
findings.push(...fileFindings);
|
|
32
|
+
scannedFiles.push({
|
|
33
|
+
path: file.relativePath,
|
|
34
|
+
bytes: file.bytes,
|
|
35
|
+
matchedFindings: fileFindings.length
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const sortedFindings = sortFindings(findings);
|
|
39
|
+
const summary = summarize(sortedFindings, scannedFiles.length);
|
|
40
|
+
return {
|
|
41
|
+
target: input.target,
|
|
42
|
+
rootDir: input.rootDir,
|
|
43
|
+
scannedAt: new Date().toISOString(),
|
|
44
|
+
summary,
|
|
45
|
+
findings: sortedFindings,
|
|
46
|
+
scannedFiles,
|
|
47
|
+
recommendations: generateRecommendations(sortedFindings),
|
|
48
|
+
agentPrompt: generateAgentPrompt(sortedFindings)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function scanContent(input) {
|
|
52
|
+
const findings = [];
|
|
53
|
+
const lines = input.content.split(/\r?\n/);
|
|
54
|
+
for (const rule of input.rules) {
|
|
55
|
+
if (rule.id === "npm-lifecycle-postinstall") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (rule.fileGlobs?.length && !matchesAnyGlob(input.relativePath, rule.fileGlobs)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const regexes = rule.patterns.map((pattern) => new RegExp(pattern, "giu"));
|
|
62
|
+
for (const regex of regexes) {
|
|
63
|
+
for (const [index, line] of lines.entries()) {
|
|
64
|
+
regex.lastIndex = 0;
|
|
65
|
+
const match = regex.exec(line);
|
|
66
|
+
if (!match) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const snippet = compactSnippet(line);
|
|
70
|
+
if (rule.category === "network-exfiltration" &&
|
|
71
|
+
allMatchedDomainsAllowed(snippet, input.allowDomains)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
findings.push({
|
|
75
|
+
ruleId: rule.id,
|
|
76
|
+
title: rule.title,
|
|
77
|
+
severity: rule.severity,
|
|
78
|
+
category: rule.category,
|
|
79
|
+
filePath: input.relativePath,
|
|
80
|
+
line: index + 1,
|
|
81
|
+
column: match.index + 1,
|
|
82
|
+
snippet,
|
|
83
|
+
explainZh: rule.explainZh,
|
|
84
|
+
safeFix: rule.safeFix
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return dedupeFindings(findings);
|
|
90
|
+
}
|
|
91
|
+
function matchesAnyGlob(filePath, globs) {
|
|
92
|
+
return globs.some((glob) => picomatch.isMatch(filePath, glob, { dot: true }));
|
|
93
|
+
}
|
|
94
|
+
function compactSnippet(line) {
|
|
95
|
+
const normalized = line.trim().replace(/\s+/g, " ");
|
|
96
|
+
return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
|
|
97
|
+
}
|
|
98
|
+
function dedupeFindings(findings) {
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
return findings.filter((finding) => {
|
|
101
|
+
const key = [
|
|
102
|
+
finding.ruleId,
|
|
103
|
+
finding.filePath,
|
|
104
|
+
finding.line,
|
|
105
|
+
finding.column,
|
|
106
|
+
finding.snippet
|
|
107
|
+
].join(":");
|
|
108
|
+
if (seen.has(key)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
seen.add(key);
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function sortFindings(findings) {
|
|
116
|
+
return [...findings].sort((a, b) => {
|
|
117
|
+
const severityDelta = severityRank[b.severity] - severityRank[a.severity];
|
|
118
|
+
if (severityDelta !== 0) {
|
|
119
|
+
return severityDelta;
|
|
120
|
+
}
|
|
121
|
+
return `${a.filePath}:${a.line}`.localeCompare(`${b.filePath}:${b.line}`);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function summarize(findings, scannedFiles) {
|
|
125
|
+
const bySeverity = { ...emptySeverityCounts };
|
|
126
|
+
for (const finding of findings) {
|
|
127
|
+
bySeverity[finding.severity] += 1;
|
|
128
|
+
}
|
|
129
|
+
const rawScore = findings.reduce((score, finding) => score + severityWeight[finding.severity], 0);
|
|
130
|
+
return {
|
|
131
|
+
score: Math.min(100, rawScore),
|
|
132
|
+
totalFindings: findings.length,
|
|
133
|
+
bySeverity,
|
|
134
|
+
scannedFiles
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const packageLifecycleScripts = ["preinstall", "install", "postinstall", "prepare"];
|
|
2
|
+
export function scanStructuredFile(input) {
|
|
3
|
+
if (input.relativePath.endsWith("package.json")) {
|
|
4
|
+
return scanPackageJson(input.relativePath, input.content, input.rules);
|
|
5
|
+
}
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
function scanPackageJson(relativePath, content, rules) {
|
|
9
|
+
const rule = rules.find((item) => item.id === "npm-lifecycle-postinstall");
|
|
10
|
+
if (!rule) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const parsed = parseJson(content);
|
|
14
|
+
const scripts = parsed?.scripts;
|
|
15
|
+
if (!scripts || typeof scripts !== "object" || Array.isArray(scripts)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const scriptName of packageLifecycleScripts) {
|
|
20
|
+
const value = scripts[scriptName];
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const location = findJsonKeyLocation(content, scriptName);
|
|
25
|
+
findings.push({
|
|
26
|
+
ruleId: rule.id,
|
|
27
|
+
title: `${rule.title}:${scriptName}`,
|
|
28
|
+
severity: rule.severity,
|
|
29
|
+
category: rule.category,
|
|
30
|
+
filePath: relativePath,
|
|
31
|
+
line: location.line,
|
|
32
|
+
column: location.column,
|
|
33
|
+
snippet: `"${scriptName}": "${value}"`,
|
|
34
|
+
explainZh: rule.explainZh,
|
|
35
|
+
safeFix: rule.safeFix
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return findings;
|
|
39
|
+
}
|
|
40
|
+
function parseJson(content) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(content);
|
|
43
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
44
|
+
? parsed
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function findJsonKeyLocation(content, key) {
|
|
52
|
+
const lines = content.split(/\r?\n/);
|
|
53
|
+
const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:`);
|
|
54
|
+
for (const [index, line] of lines.entries()) {
|
|
55
|
+
const match = keyPattern.exec(line);
|
|
56
|
+
if (match) {
|
|
57
|
+
return {
|
|
58
|
+
line: index + 1,
|
|
59
|
+
column: match.index + 1
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
line: 1,
|
|
65
|
+
column: 1
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function escapeRegExp(value) {
|
|
69
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=structured.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured.js","sourceRoot":"","sources":["../src/structured.ts"],"names":[],"mappings":"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"}
|
package/dist/target.d.ts
ADDED
package/dist/target.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { simpleGit } from "simple-git";
|
|
5
|
+
const remoteTargetPattern = /^(https?:\/\/|git@|ssh:\/\/|[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+:).+/;
|
|
6
|
+
export function isRemoteTarget(target) {
|
|
7
|
+
return remoteTargetPattern.test(target);
|
|
8
|
+
}
|
|
9
|
+
export async function resolveTarget(target, gitBinary) {
|
|
10
|
+
if (!isRemoteTarget(target)) {
|
|
11
|
+
const rootDir = path.resolve(target);
|
|
12
|
+
const stat = await fs.stat(rootDir).catch(() => undefined);
|
|
13
|
+
if (!stat?.isDirectory()) {
|
|
14
|
+
throw new Error(`目标不是可扫描目录: ${rootDir}`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
original: target,
|
|
18
|
+
rootDir,
|
|
19
|
+
isRemote: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agentsafe-"));
|
|
23
|
+
const repoDir = path.join(tempRoot, "repo");
|
|
24
|
+
const git = simpleGit();
|
|
25
|
+
const binary = gitBinary ?? process.env.AGENTSAFE_GIT_BINARY ?? process.env.GIT_BINARY;
|
|
26
|
+
if (binary) {
|
|
27
|
+
git.customBinary(binary);
|
|
28
|
+
}
|
|
29
|
+
await git.clone(target, repoDir, ["--depth", "1"]);
|
|
30
|
+
return {
|
|
31
|
+
original: target,
|
|
32
|
+
rootDir: repoDir,
|
|
33
|
+
isRemote: true,
|
|
34
|
+
cleanup: async () => {
|
|
35
|
+
await fs.rm(tempRoot, { force: true, recursive: true });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target.js","sourceRoot":"","sources":["../src/target.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,MAAM,mBAAmB,GAAG,iEAAiE,CAAC;AAE9F,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,SAAkB;IACpE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAE3D,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,cAAc,OAAO,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,OAAO;YACL,QAAQ,EAAE,MAAM;YAChB,OAAO;YACP,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,YAAY,CAAC,CAAC,CAAC;IACxE,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,MAAM,MAAM,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;IAEvF,IAAI,MAAM,EAAE,CAAC;QACX,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;IAEnD,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,OAAO;QAChB,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type Severity = "low" | "medium" | "high" | "block";
|
|
2
|
+
export interface Rule {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
severity: Severity;
|
|
6
|
+
category: string;
|
|
7
|
+
patterns: string[];
|
|
8
|
+
fileGlobs?: string[];
|
|
9
|
+
explainZh: string;
|
|
10
|
+
safeFix: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RawRuleFile {
|
|
13
|
+
rules: Array<{
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
severity: Severity;
|
|
17
|
+
category: string;
|
|
18
|
+
patterns: string[];
|
|
19
|
+
file_globs?: string[];
|
|
20
|
+
explain_zh: string;
|
|
21
|
+
safe_fix: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface Finding {
|
|
25
|
+
ruleId: string;
|
|
26
|
+
title: string;
|
|
27
|
+
severity: Severity;
|
|
28
|
+
category: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
line: number;
|
|
31
|
+
column: number;
|
|
32
|
+
snippet: string;
|
|
33
|
+
explainZh: string;
|
|
34
|
+
safeFix: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ScannedFile {
|
|
37
|
+
path: string;
|
|
38
|
+
bytes: number;
|
|
39
|
+
matchedFindings: number;
|
|
40
|
+
}
|
|
41
|
+
export interface ScanSummary {
|
|
42
|
+
score: number;
|
|
43
|
+
totalFindings: number;
|
|
44
|
+
bySeverity: Record<Severity, number>;
|
|
45
|
+
scannedFiles: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ScanReport {
|
|
48
|
+
target: string;
|
|
49
|
+
rootDir: string;
|
|
50
|
+
scannedAt: string;
|
|
51
|
+
summary: ScanSummary;
|
|
52
|
+
findings: Finding[];
|
|
53
|
+
scannedFiles: ScannedFile[];
|
|
54
|
+
agentPrompt: string;
|
|
55
|
+
recommendations: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface ScanOptions {
|
|
58
|
+
target: string;
|
|
59
|
+
outDir?: string;
|
|
60
|
+
allowDomains?: string[];
|
|
61
|
+
gitBinary?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface ResolvedTarget {
|
|
64
|
+
original: string;
|
|
65
|
+
rootDir: string;
|
|
66
|
+
isRemote: boolean;
|
|
67
|
+
cleanup?: () => Promise<void>;
|
|
68
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentsafe-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AgentSafe: AI Agent 开源项目安装前安全体检 CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentsafe": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"rules"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"scan:sample": "tsx src/index.ts scan test/fixtures/risky-project -o tmp/sample-report",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai-agent",
|
|
22
|
+
"security",
|
|
23
|
+
"supply-chain",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^14.0.2",
|
|
29
|
+
"fast-glob": "^3.3.3",
|
|
30
|
+
"picomatch": "^4.0.3",
|
|
31
|
+
"simple-git": "^3.29.0",
|
|
32
|
+
"smol-toml": "^1.4.2",
|
|
33
|
+
"yaml": "^2.8.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.10.1",
|
|
37
|
+
"tsx": "^4.20.6",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"vitest": "^4.0.15"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"packageManager": "pnpm@11.7.0"
|
|
45
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
rules:
|
|
2
|
+
- id: shell-curl-pipe-bash
|
|
3
|
+
title: curl 下载内容后直接交给 shell 执行
|
|
4
|
+
severity: block
|
|
5
|
+
category: remote-code-execution
|
|
6
|
+
patterns:
|
|
7
|
+
- "\\b(curl|wget)\\b[^\\n|;]*(\\||>)\\s*(sudo\\s+)?(bash|sh|zsh)\\b"
|
|
8
|
+
explain_zh: "该命令会把远程下载内容直接交给 shell 执行,安装前无法确认真实执行内容。"
|
|
9
|
+
safe_fix: "不要让 Agent 自动运行;先下载到临时文件人工审阅,并在沙箱环境执行。"
|
|
10
|
+
- id: shell-wget-pipe-sh
|
|
11
|
+
title: wget 下载内容后直接执行
|
|
12
|
+
severity: block
|
|
13
|
+
category: remote-code-execution
|
|
14
|
+
patterns:
|
|
15
|
+
- "\\bwget\\b[^\\n|;]*\\|\\s*(sudo\\s+)?(bash|sh|zsh)\\b"
|
|
16
|
+
explain_zh: "该命令会把 wget 获取的远程脚本直接执行,属于阻断级风险。"
|
|
17
|
+
safe_fix: "改为固定版本下载、校验哈希后再手动执行。"
|
|
18
|
+
- id: shell-eval-remote
|
|
19
|
+
title: eval/exec 执行动态字符串
|
|
20
|
+
severity: high
|
|
21
|
+
category: dynamic-execution
|
|
22
|
+
patterns:
|
|
23
|
+
- "\\b(eval|exec)\\b\\s+[`\"'$]?"
|
|
24
|
+
explain_zh: "eval/exec 会执行运行时拼接的字符串,容易隐藏真实行为。"
|
|
25
|
+
safe_fix: "要求人工确认字符串来源,避免自动修复流程继续执行。"
|
|
26
|
+
- id: shell-base64-exec
|
|
27
|
+
title: base64 解码后执行
|
|
28
|
+
severity: block
|
|
29
|
+
category: obfuscation
|
|
30
|
+
patterns:
|
|
31
|
+
- "base64\\s+(-d|--decode)[^\\n|;]*(\\||;|&&).*(bash|sh|zsh|python|node|powershell|pwsh)"
|
|
32
|
+
explain_zh: "脚本会解码隐藏内容后执行,常用于混淆真实载荷。"
|
|
33
|
+
safe_fix: "先解码并审阅内容,不允许 Agent 自动运行该链路。"
|
|
34
|
+
- id: shell-dns-txt-exec
|
|
35
|
+
title: DNS TXT 查询结果进入执行链
|
|
36
|
+
severity: block
|
|
37
|
+
category: remote-code-execution
|
|
38
|
+
patterns:
|
|
39
|
+
- "\\b(dig|nslookup|Resolve-DnsName)\\b[^\\n]*(TXT|-type=txt|-Type\\s+TXT)[^\\n]*(\\||;|&&).*(bash|sh|eval|exec|python|powershell|pwsh)"
|
|
40
|
+
explain_zh: "该脚本会从 DNS TXT 记录读取字符串并执行,属于强风险行为。"
|
|
41
|
+
safe_fix: "阻止自动执行,定位域名来源并在隔离环境复核。"
|
|
42
|
+
- id: npm-lifecycle-postinstall
|
|
43
|
+
title: npm 安装生命周期脚本
|
|
44
|
+
severity: high
|
|
45
|
+
category: package-lifecycle
|
|
46
|
+
patterns:
|
|
47
|
+
- "\"(preinstall|install|postinstall|prepare)\"\\s*:"
|
|
48
|
+
explain_zh: "npm/pnpm/yarn install 时可能自动运行该脚本,Agent 修复安装失败时容易触发。"
|
|
49
|
+
safe_fix: "优先使用 npm/pnpm/yarn 的 --ignore-scripts,再人工审阅脚本内容。"
|
|
50
|
+
- id: credential-ssh-path
|
|
51
|
+
title: 读取 SSH 密钥目录
|
|
52
|
+
severity: block
|
|
53
|
+
category: credential-access
|
|
54
|
+
patterns:
|
|
55
|
+
- "(~|\\$HOME|%USERPROFILE%)[/\\\\]\\.ssh"
|
|
56
|
+
explain_zh: "脚本访问用户 SSH 密钥目录,可能导致开发机身份凭据泄露。"
|
|
57
|
+
safe_fix: "不要运行该脚本;如必须分析,使用没有真实凭据的沙箱账户。"
|
|
58
|
+
- id: credential-env-path
|
|
59
|
+
title: 读取 .env 或环境密钥文件
|
|
60
|
+
severity: high
|
|
61
|
+
category: credential-access
|
|
62
|
+
patterns:
|
|
63
|
+
- "(^|[^\\w.-])\\.env(\\.[A-Za-z0-9_-]+)?"
|
|
64
|
+
explain_zh: "脚本或配置引用 .env 文件,可能读取本地 API Key、数据库密码等敏感信息。"
|
|
65
|
+
safe_fix: "确认读取用途,使用示例环境变量文件替代真实密钥。"
|
|
66
|
+
- id: credential-cloud-path
|
|
67
|
+
title: 读取云厂商凭据目录
|
|
68
|
+
severity: block
|
|
69
|
+
category: credential-access
|
|
70
|
+
patterns:
|
|
71
|
+
- "(~|\\$HOME|%USERPROFILE%)[/\\\\]\\.(aws|azure|config[/\\\\]gcloud)"
|
|
72
|
+
explain_zh: "脚本访问云厂商凭据目录,可能造成云账号权限泄露。"
|
|
73
|
+
safe_fix: "在无云凭据的隔离用户中执行,并限制网络访问。"
|
|
74
|
+
- id: browser-secret-path
|
|
75
|
+
title: 访问浏览器数据目录
|
|
76
|
+
severity: block
|
|
77
|
+
category: credential-access
|
|
78
|
+
patterns:
|
|
79
|
+
- "(Chrome|Chromium|Edge|Firefox|Login Data|Cookies|Local State)"
|
|
80
|
+
explain_zh: "脚本提到浏览器数据目录,可能涉及 Cookie、登录态或本地加密材料。"
|
|
81
|
+
safe_fix: "不要在主力开发机运行,使用干净容器或虚拟机。"
|
|
82
|
+
- id: network-upload-curl
|
|
83
|
+
title: curl/wget 上传本地文件
|
|
84
|
+
severity: high
|
|
85
|
+
category: network-exfiltration
|
|
86
|
+
patterns:
|
|
87
|
+
- "\\b(curl|wget)\\b[^\\n]*(\\s--upload-file\\s|\\s-T\\s|\\s-F\\s|\\s--form\\s|\\s--data-binary\\s|\\s--data-raw\\s|\\s--data\\s)[^\\n]*https?://"
|
|
88
|
+
explain_zh: "命令可能把本地文件或数据上传到外部地址。"
|
|
89
|
+
safe_fix: "确认目标域名可信;未知域名需要阻断并人工复核。"
|
|
90
|
+
- id: network-netcat
|
|
91
|
+
title: 使用 nc/netcat 建立网络通道
|
|
92
|
+
severity: high
|
|
93
|
+
category: network-exfiltration
|
|
94
|
+
patterns:
|
|
95
|
+
- "\\b(nc|netcat|ncat)\\b\\s+(-e|-c|[A-Za-z0-9.-]+\\s+\\d{2,5})"
|
|
96
|
+
explain_zh: "nc/netcat 可建立原始网络通道,常被用于反连或数据转移。"
|
|
97
|
+
safe_fix: "禁止自动运行,确认业务是否真的需要该网络通道。"
|
|
98
|
+
- id: powershell-encoded-command
|
|
99
|
+
title: PowerShell EncodedCommand
|
|
100
|
+
severity: block
|
|
101
|
+
category: obfuscation
|
|
102
|
+
patterns:
|
|
103
|
+
- "\\b(powershell|pwsh)\\b[^\\n]*-(enc|encodedcommand|EncodedCommand)\\b"
|
|
104
|
+
explain_zh: "PowerShell EncodedCommand 会隐藏真实命令内容,属于阻断级风险。"
|
|
105
|
+
safe_fix: "先解码命令并审阅,禁止 Agent 自动执行。"
|
|
106
|
+
- id: python-one-liner-exec
|
|
107
|
+
title: Python one-liner 动态执行
|
|
108
|
+
severity: high
|
|
109
|
+
category: dynamic-execution
|
|
110
|
+
patterns:
|
|
111
|
+
- "\\bpython(3)?\\b\\s+-c\\s+[\"'][^\"']*(exec|eval|compile|__import__)"
|
|
112
|
+
explain_zh: "Python -c 动态执行代码,可能绕过普通文件审阅。"
|
|
113
|
+
safe_fix: "展开为可审阅脚本,确认无凭据读取和外联行为后再运行。"
|
|
114
|
+
- id: node-one-liner-exec
|
|
115
|
+
title: Node.js one-liner 动态执行
|
|
116
|
+
severity: high
|
|
117
|
+
category: dynamic-execution
|
|
118
|
+
patterns:
|
|
119
|
+
- "\\bnode\\b\\s+-e\\s+[\"'][^\"']*(eval|Function|child_process)"
|
|
120
|
+
explain_zh: "Node -e 动态执行代码,可能在安装流程中执行隐藏逻辑。"
|
|
121
|
+
safe_fix: "展开代码并人工审阅,避免自动修复命令继续执行。"
|
|
122
|
+
- id: shell-ifs-obfuscation
|
|
123
|
+
title: Shell IFS 混淆
|
|
124
|
+
severity: medium
|
|
125
|
+
category: obfuscation
|
|
126
|
+
patterns:
|
|
127
|
+
- "\\$\\{?IFS\\}?"
|
|
128
|
+
explain_zh: "脚本使用 IFS 混淆空格或命令结构,降低可读性。"
|
|
129
|
+
safe_fix: "把命令展开后再判断,未知来源脚本建议沙箱执行。"
|
|
130
|
+
- id: docker-privileged
|
|
131
|
+
title: Docker privileged 或挂载宿主敏感目录
|
|
132
|
+
severity: high
|
|
133
|
+
category: sandbox-escape-risk
|
|
134
|
+
patterns:
|
|
135
|
+
- "docker\\s+run[^\\n]*(--privileged|-v\\s+/(var/run/docker.sock|:/|~|\\$HOME))"
|
|
136
|
+
explain_zh: "Docker 命令可能授予容器过高权限或挂载宿主敏感目录。"
|
|
137
|
+
safe_fix: "去掉 privileged 和敏感挂载,使用最小权限容器。"
|
|
138
|
+
- id: dockerfile-remote-add
|
|
139
|
+
title: Dockerfile 从远程 URL ADD 内容
|
|
140
|
+
severity: medium
|
|
141
|
+
category: supply-chain
|
|
142
|
+
patterns:
|
|
143
|
+
- "^\\s*ADD\\s+https?://"
|
|
144
|
+
explain_zh: "Dockerfile 使用 ADD 从远程地址拉取内容,构建时内容可能变化。"
|
|
145
|
+
safe_fix: "改为固定版本下载并校验哈希,或把依赖纳入锁定流程。"
|
|
146
|
+
- id: github-action-unpinned
|
|
147
|
+
title: GitHub Action 未固定 commit
|
|
148
|
+
severity: medium
|
|
149
|
+
category: ci-supply-chain
|
|
150
|
+
patterns:
|
|
151
|
+
- "uses:\\s+[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@(main|master|latest|v\\d+)"
|
|
152
|
+
explain_zh: "CI Action 未固定到 commit SHA,供应链内容可能随上游变化。"
|
|
153
|
+
safe_fix: "固定到完整 commit SHA,并定期人工升级。"
|
|
154
|
+
- id: chmod-recursive
|
|
155
|
+
title: 递归 chmod 宽权限
|
|
156
|
+
severity: medium
|
|
157
|
+
category: destructive-or-broad-access
|
|
158
|
+
patterns:
|
|
159
|
+
- "chmod\\s+(-R\\s+)?(777|a\\+rw|a\\+rwx)"
|
|
160
|
+
explain_zh: "递归开放权限可能扩大本地文件被修改或读取的范围。"
|
|
161
|
+
safe_fix: "缩小目录范围并使用最小权限。"
|