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 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`:结构化扫描结果
@@ -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"}
@@ -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"}
@@ -0,0 +1,7 @@
1
+ export interface CollectedFile {
2
+ absolutePath: string;
3
+ relativePath: string;
4
+ content: string;
5
+ bytes: number;
6
+ }
7
+ export declare function collectFiles(rootDir: string): Promise<CollectedFile[]>;
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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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"}
@@ -0,0 +1,3 @@
1
+ import type { Finding } from "./types.js";
2
+ export declare function generateRecommendations(findings: Finding[]): string[];
3
+ export declare function generateAgentPrompt(findings: Finding[]): string;
@@ -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"}
@@ -0,0 +1,6 @@
1
+ import type { ScanReport } from "./types.js";
2
+ export declare function writeReports(report: ScanReport, outDir: string): Promise<{
3
+ markdownPath: string;
4
+ jsonPath: string;
5
+ }>;
6
+ export declare function renderMarkdownReport(report: ScanReport): string;
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":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "./types.js";
2
+ export declare function loadRules(ruleFile: string): Promise<Rule[]>;
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"}
@@ -0,0 +1,8 @@
1
+ import type { Rule, ScanReport } from "./types.js";
2
+ export interface RunScanInput {
3
+ target: string;
4
+ rootDir: string;
5
+ rules: Rule[];
6
+ allowDomains?: string[];
7
+ }
8
+ export declare function runScan(input: RunScanInput): Promise<ScanReport>;
@@ -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,6 @@
1
+ import type { Finding, Rule } from "./types.js";
2
+ export declare function scanStructuredFile(input: {
3
+ relativePath: string;
4
+ content: string;
5
+ rules: Rule[];
6
+ }): Finding[];
@@ -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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"}
@@ -0,0 +1,3 @@
1
+ import type { ResolvedTarget } from "./types.js";
2
+ export declare function isRemoteTarget(target: string): boolean;
3
+ export declare function resolveTarget(target: string, gitBinary?: string): Promise<ResolvedTarget>;
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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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: "缩小目录范围并使用最小权限。"