@elliotllliu/agentshield 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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +43 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/reporter/json.d.ts +3 -0
  7. package/dist/reporter/json.js +5 -0
  8. package/dist/reporter/json.js.map +1 -0
  9. package/dist/reporter/terminal.d.ts +2 -0
  10. package/dist/reporter/terminal.js +60 -0
  11. package/dist/reporter/terminal.js.map +1 -0
  12. package/dist/rules/backdoor.d.ts +2 -0
  13. package/dist/rules/backdoor.js +57 -0
  14. package/dist/rules/backdoor.js.map +1 -0
  15. package/dist/rules/credential-hardcode.d.ts +2 -0
  16. package/dist/rules/credential-hardcode.js +57 -0
  17. package/dist/rules/credential-hardcode.js.map +1 -0
  18. package/dist/rules/crypto-mining.d.ts +2 -0
  19. package/dist/rules/crypto-mining.js +41 -0
  20. package/dist/rules/crypto-mining.js.map +1 -0
  21. package/dist/rules/data-exfil.d.ts +2 -0
  22. package/dist/rules/data-exfil.js +61 -0
  23. package/dist/rules/data-exfil.js.map +1 -0
  24. package/dist/rules/env-leak.d.ts +2 -0
  25. package/dist/rules/env-leak.js +43 -0
  26. package/dist/rules/env-leak.js.map +1 -0
  27. package/dist/rules/excessive-perms.d.ts +2 -0
  28. package/dist/rules/excessive-perms.js +50 -0
  29. package/dist/rules/excessive-perms.js.map +1 -0
  30. package/dist/rules/hidden-files.d.ts +2 -0
  31. package/dist/rules/hidden-files.js +52 -0
  32. package/dist/rules/hidden-files.js.map +1 -0
  33. package/dist/rules/index.d.ts +5 -0
  34. package/dist/rules/index.js +40 -0
  35. package/dist/rules/index.js.map +1 -0
  36. package/dist/rules/network-ssrf.d.ts +2 -0
  37. package/dist/rules/network-ssrf.js +51 -0
  38. package/dist/rules/network-ssrf.js.map +1 -0
  39. package/dist/rules/obfuscation.d.ts +2 -0
  40. package/dist/rules/obfuscation.js +51 -0
  41. package/dist/rules/obfuscation.js.map +1 -0
  42. package/dist/rules/phone-home.d.ts +2 -0
  43. package/dist/rules/phone-home.js +38 -0
  44. package/dist/rules/phone-home.js.map +1 -0
  45. package/dist/rules/privilege.d.ts +2 -0
  46. package/dist/rules/privilege.js +111 -0
  47. package/dist/rules/privilege.js.map +1 -0
  48. package/dist/rules/reverse-shell.d.ts +2 -0
  49. package/dist/rules/reverse-shell.js +53 -0
  50. package/dist/rules/reverse-shell.js.map +1 -0
  51. package/dist/rules/sensitive-read.d.ts +2 -0
  52. package/dist/rules/sensitive-read.js +53 -0
  53. package/dist/rules/sensitive-read.js.map +1 -0
  54. package/dist/rules/supply-chain.d.ts +6 -0
  55. package/dist/rules/supply-chain.js +105 -0
  56. package/dist/rules/supply-chain.js.map +1 -0
  57. package/dist/rules/typosquatting.d.ts +2 -0
  58. package/dist/rules/typosquatting.js +56 -0
  59. package/dist/rules/typosquatting.js.map +1 -0
  60. package/dist/scanner/files.d.ts +5 -0
  61. package/dist/scanner/files.js +67 -0
  62. package/dist/scanner/files.js.map +1 -0
  63. package/dist/scanner/index.d.ts +3 -0
  64. package/dist/scanner/index.js +24 -0
  65. package/dist/scanner/index.js.map +1 -0
  66. package/dist/score.d.ts +14 -0
  67. package/dist/score.js +35 -0
  68. package/dist/score.js.map +1 -0
  69. package/dist/types.d.ts +42 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +33 -0
@@ -0,0 +1,105 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ /**
5
+ * Rule: supply-chain
6
+ * Runs npm audit to detect known CVEs in dependencies.
7
+ */
8
+ export const supplyChainRule = {
9
+ id: "supply-chain",
10
+ name: "Supply Chain Audit",
11
+ description: "Checks for known CVEs in npm/pip dependencies",
12
+ run(files) {
13
+ const findings = [];
14
+ // Determine the target directory from the first file
15
+ if (files.length === 0)
16
+ return findings;
17
+ const targetDir = files[0].path.replace(files[0].relativePath, "").replace(/\/$/, "");
18
+ // Check for package.json
19
+ const pkgJsonPath = join(targetDir, "package.json");
20
+ const hasNodeModules = existsSync(join(targetDir, "node_modules"));
21
+ const hasPkgJson = existsSync(pkgJsonPath);
22
+ const hasLockFile = existsSync(join(targetDir, "package-lock.json")) ||
23
+ existsSync(join(targetDir, "yarn.lock")) ||
24
+ existsSync(join(targetDir, "pnpm-lock.yaml"));
25
+ if (hasPkgJson && (hasNodeModules || hasLockFile)) {
26
+ try {
27
+ // npm audit returns exit code > 0 when vulnerabilities are found
28
+ const result = execSync("npm audit --json 2>/dev/null", {
29
+ cwd: targetDir,
30
+ encoding: "utf-8",
31
+ timeout: 30000,
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+ parseNpmAudit(result, findings);
35
+ }
36
+ catch (err) {
37
+ // npm audit exits non-zero when vulns are found
38
+ if (err && typeof err === "object" && "stdout" in err) {
39
+ parseNpmAudit(err.stdout, findings);
40
+ }
41
+ }
42
+ }
43
+ else if (hasPkgJson) {
44
+ // Has package.json but no lock file — check deps manually
45
+ try {
46
+ const pkgContent = files.find((f) => f.relativePath === "package.json");
47
+ if (pkgContent) {
48
+ const pkg = JSON.parse(pkgContent.content);
49
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
50
+ const depCount = Object.keys(deps).length;
51
+ if (depCount > 0) {
52
+ findings.push({
53
+ rule: "supply-chain",
54
+ severity: "info",
55
+ file: "package.json",
56
+ message: `${depCount} dependencies declared — run 'npm install && npm audit' for full CVE check`,
57
+ });
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // ignore parse errors
63
+ }
64
+ }
65
+ // Check for requirements.txt (Python)
66
+ const reqTxt = files.find((f) => f.relativePath === "requirements.txt" || f.relativePath.endsWith("/requirements.txt"));
67
+ if (reqTxt) {
68
+ findings.push({
69
+ rule: "supply-chain",
70
+ severity: "info",
71
+ file: reqTxt.relativePath,
72
+ message: "Python requirements.txt found — run 'pip-audit' for CVE check",
73
+ });
74
+ }
75
+ return findings;
76
+ },
77
+ };
78
+ function parseNpmAudit(output, findings) {
79
+ try {
80
+ const audit = JSON.parse(output);
81
+ if (audit.vulnerabilities) {
82
+ for (const [name, vuln] of Object.entries(audit.vulnerabilities)) {
83
+ const severity = vuln.severity || "moderate";
84
+ const via = Array.isArray(vuln.via) ? vuln.via : [];
85
+ const cves = via
86
+ .filter((v) => typeof v === "object" && v !== null && "url" in v)
87
+ .map((v) => String(v.url))
88
+ .join(", ");
89
+ const mappedSeverity = severity === "critical" || severity === "high" ? "critical" :
90
+ severity === "moderate" ? "warning" : "info";
91
+ findings.push({
92
+ rule: "supply-chain",
93
+ severity: mappedSeverity,
94
+ file: "package.json",
95
+ message: `${name} — ${severity} severity${cves ? ` (${cves})` : ""}`,
96
+ evidence: typeof vuln.range === "string" ? `affected: ${vuln.range}` : undefined,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ // unparseable output
103
+ }
104
+ }
105
+ //# sourceMappingURL=supply-chain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supply-chain.js","sourceRoot":"","sources":["../../src/rules/supply-chain.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const typosquattingRule: Rule;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Rule: typosquatting
3
+ * Detects potentially typosquatted npm package names.
4
+ */
5
+ // Known popular packages and their common typos
6
+ const TYPOSQUAT_MAP = {
7
+ lodash: ["1odash", "lodsh", "lodashs", "lodahs"],
8
+ express: ["expresss", "expres", "exress", "exppress"],
9
+ axios: ["axois", "axio", "axioss", "axiso"],
10
+ react: ["raect", "reacct", "reactt"],
11
+ chalk: ["chalks", "chalkk", "chak"],
12
+ commander: ["comander", "commanderr", "commmander"],
13
+ "node-fetch": ["node-ftch", "nodefetch", "node-fetchh"],
14
+ request: ["reqeust", "requets", "reuqest"],
15
+ mongoose: ["mongose", "mongosse", "mongooose"],
16
+ webpack: ["webpck", "webpackk", "weback"],
17
+ eslint: ["eslintt", "eslnt", "elint"],
18
+ typescript: ["typscript", "typescipt", "typesript"],
19
+ };
20
+ export const typosquattingRule = {
21
+ id: "typosquatting",
22
+ name: "Dependency Typosquatting",
23
+ description: "Detects potentially typosquatted package names in dependencies",
24
+ run(files) {
25
+ const findings = [];
26
+ const pkgJson = files.find((f) => f.relativePath === "package.json" || f.relativePath.endsWith("/package.json"));
27
+ if (!pkgJson)
28
+ return findings;
29
+ try {
30
+ const pkg = JSON.parse(pkgJson.content);
31
+ const allDeps = {
32
+ ...pkg.dependencies,
33
+ ...pkg.devDependencies,
34
+ ...pkg.peerDependencies,
35
+ ...pkg.optionalDependencies,
36
+ };
37
+ for (const depName of Object.keys(allDeps)) {
38
+ for (const [legitimate, typos] of Object.entries(TYPOSQUAT_MAP)) {
39
+ if (typos.includes(depName.toLowerCase())) {
40
+ findings.push({
41
+ rule: "typosquatting",
42
+ severity: "critical",
43
+ file: pkgJson.relativePath,
44
+ message: `Suspicious package "${depName}" — possible typosquat of "${legitimate}"`,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ // ignore parse errors
52
+ }
53
+ return findings;
54
+ },
55
+ };
56
+ //# sourceMappingURL=typosquatting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typosquatting.js","sourceRoot":"","sources":["../../src/rules/typosquatting.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,gDAAgD;AAChD,MAAM,aAAa,GAA6B;IAC9C,MAAM,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC;IAChD,OAAO,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,CAAC;IACrD,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC;IAC3C,KAAK,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACpC,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC;IACnC,SAAS,EAAE,CAAC,UAAU,EAAE,YAAY,EAAE,YAAY,CAAC;IACnD,YAAY,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,aAAa,CAAC;IACvD,OAAO,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC;IAC1C,QAAQ,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC;IAC9C,OAAO,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC;IACzC,MAAM,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC;IACrC,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC;CACpD,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAS;IACrC,EAAE,EAAE,eAAe;IACnB,IAAI,EAAE,0BAA0B;IAChC,WAAW,EAAE,gEAAgE;IAE7E,GAAG,CAAC,KAAoB;QACtB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAE/B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CACxB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,cAAc,IAAI,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,eAAe,CAAC,CACrF,CAAC;QACF,IAAI,CAAC,OAAO;YAAE,OAAO,QAAQ,CAAC;QAE9B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACxC,MAAM,OAAO,GAAG;gBACd,GAAG,GAAG,CAAC,YAAY;gBACnB,GAAG,GAAG,CAAC,eAAe;gBACtB,GAAG,GAAG,CAAC,gBAAgB;gBACvB,GAAG,GAAG,CAAC,oBAAoB;aAC5B,CAAC;YAEF,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3C,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;oBAChE,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;wBAC1C,QAAQ,CAAC,IAAI,CAAC;4BACZ,IAAI,EAAE,eAAe;4BACrB,QAAQ,EAAE,UAAU;4BACpB,IAAI,EAAE,OAAO,CAAC,YAAY;4BAC1B,OAAO,EAAE,uBAAuB,OAAO,8BAA8B,UAAU,GAAG;yBACnF,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { ScannedFile } from "../types.js";
2
+ /** Recursively collect scannable files from a directory */
3
+ export declare function collectFiles(dir: string, base?: string): ScannedFile[];
4
+ /** Count total lines across files */
5
+ export declare function totalLines(files: ScannedFile[]): number;
@@ -0,0 +1,67 @@
1
+ import { readFileSync, statSync, readdirSync } from "fs";
2
+ import { join, relative, extname } from "path";
3
+ const SKIP_DIRS = new Set([
4
+ "node_modules", ".git", "dist", "build", "__pycache__", ".venv", "venv",
5
+ ]);
6
+ const CODE_EXTS = new Set([
7
+ ".ts", ".js", ".mjs", ".cjs", ".tsx", ".jsx",
8
+ ".py", ".sh", ".bash", ".zsh",
9
+ ".json", ".yaml", ".yml", ".toml",
10
+ ".md",
11
+ ]);
12
+ const MAX_FILE_SIZE = 512 * 1024; // 512 KB
13
+ /** Recursively collect scannable files from a directory */
14
+ export function collectFiles(dir, base) {
15
+ const root = base ?? dir;
16
+ const files = [];
17
+ let entries;
18
+ try {
19
+ entries = readdirSync(dir);
20
+ }
21
+ catch {
22
+ return files;
23
+ }
24
+ for (const name of entries) {
25
+ if (name.startsWith(".") && name !== ".env")
26
+ continue;
27
+ if (SKIP_DIRS.has(name))
28
+ continue;
29
+ const fullPath = join(dir, name);
30
+ let stat;
31
+ try {
32
+ stat = statSync(fullPath);
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ if (stat.isDirectory()) {
38
+ files.push(...collectFiles(fullPath, root));
39
+ }
40
+ else if (stat.isFile()) {
41
+ const ext = extname(name).toLowerCase();
42
+ if (!CODE_EXTS.has(ext) && name !== "SKILL.md")
43
+ continue;
44
+ if (stat.size > MAX_FILE_SIZE)
45
+ continue;
46
+ try {
47
+ const content = readFileSync(fullPath, "utf-8");
48
+ files.push({
49
+ path: fullPath,
50
+ relativePath: relative(root, fullPath),
51
+ content,
52
+ lines: content.split("\n"),
53
+ ext,
54
+ });
55
+ }
56
+ catch {
57
+ // skip unreadable files
58
+ }
59
+ }
60
+ }
61
+ return files;
62
+ }
63
+ /** Count total lines across files */
64
+ export function totalLines(files) {
65
+ return files.reduce((sum, f) => sum + f.lines.length, 0);
66
+ }
67
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/scanner/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM;CACxE,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAC5C,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM;IAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO;IACjC,KAAK;CACN,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,SAAS;AAE3C,2DAA2D;AAC3D,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,IAAa;IACrD,MAAM,IAAI,GAAG,IAAI,IAAI,GAAG,CAAC;IACzB,MAAM,KAAK,GAAkB,EAAE,CAAC;IAEhC,IAAI,OAAiB,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,MAAM;YAAE,SAAS;QACtD,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAElC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC;QACT,IAAI,CAAC;YACH,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;YACzB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,KAAK,UAAU;gBAAE,SAAS;YACzD,IAAI,IAAI,CAAC,IAAI,GAAG,aAAa;gBAAE,SAAS;YAExC,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAChD,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,QAAQ;oBACd,YAAY,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;oBACtC,OAAO;oBACP,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC;oBAC1B,GAAG;iBACJ,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,wBAAwB;YAC1B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,qCAAqC;AACrC,MAAM,UAAU,UAAU,CAAC,KAAoB;IAC7C,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ScanResult } from "../types.js";
2
+ /** Run all rules against a target directory */
3
+ export declare function scan(targetDir: string): ScanResult;
@@ -0,0 +1,24 @@
1
+ import { collectFiles, totalLines } from "./files.js";
2
+ import { rules } from "../rules/index.js";
3
+ import { computeScore } from "../score.js";
4
+ /** Run all rules against a target directory */
5
+ export function scan(targetDir) {
6
+ const start = Date.now();
7
+ const files = collectFiles(targetDir);
8
+ const findings = [];
9
+ for (const rule of rules) {
10
+ findings.push(...rule.run(files));
11
+ }
12
+ // Sort: critical first, then warning, then info
13
+ const severityOrder = { critical: 0, warning: 1, info: 2 };
14
+ findings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
15
+ return {
16
+ target: targetDir,
17
+ filesScanned: files.length,
18
+ linesScanned: totalLines(files),
19
+ findings,
20
+ score: computeScore(findings),
21
+ duration: Date.now() - start,
22
+ };
23
+ }
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scanner/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,+CAA+C;AAC/C,MAAM,UAAU,IAAI,CAAC,SAAiB;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IACpC,CAAC;IAED,gDAAgD;IAChD,MAAM,aAAa,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3D,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE/E,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,YAAY,EAAE,KAAK,CAAC,MAAM;QAC1B,YAAY,EAAE,UAAU,CAAC,KAAK,CAAC;QAC/B,QAAQ;QACR,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;QAC7B,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;KAC7B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Finding } from "./types.js";
2
+ /**
3
+ * Compute a security score from 0-100.
4
+ *
5
+ * Starts at 100, deducts points per finding:
6
+ * critical: -25
7
+ * warning: -10
8
+ * info: -0
9
+ *
10
+ * Minimum score is 0.
11
+ */
12
+ export declare function computeScore(findings: Finding[]): number;
13
+ /** Human-readable risk label */
14
+ export declare function riskLabel(score: number): string;
package/dist/score.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Compute a security score from 0-100.
3
+ *
4
+ * Starts at 100, deducts points per finding:
5
+ * critical: -25
6
+ * warning: -10
7
+ * info: -0
8
+ *
9
+ * Minimum score is 0.
10
+ */
11
+ export function computeScore(findings) {
12
+ let score = 100;
13
+ for (const f of findings) {
14
+ switch (f.severity) {
15
+ case "critical":
16
+ score -= 25;
17
+ break;
18
+ case "warning":
19
+ score -= 10;
20
+ break;
21
+ }
22
+ }
23
+ return Math.max(0, score);
24
+ }
25
+ /** Human-readable risk label */
26
+ export function riskLabel(score) {
27
+ if (score >= 90)
28
+ return "Low Risk";
29
+ if (score >= 70)
30
+ return "Moderate Risk";
31
+ if (score >= 40)
32
+ return "High Risk";
33
+ return "Critical Risk";
34
+ }
35
+ //# sourceMappingURL=score.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"score.js","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,QAAmB;IAC9C,IAAI,KAAK,GAAG,GAAG,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;YACnB,KAAK,UAAU;gBACb,KAAK,IAAI,EAAE,CAAC;gBACZ,MAAM;YACR,KAAK,SAAS;gBACZ,KAAK,IAAI,EAAE,CAAC;gBACZ,MAAM;QACV,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,SAAS,CAAC,KAAa;IACrC,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,UAAU,CAAC;IACnC,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,eAAe,CAAC;IACxC,IAAI,KAAK,IAAI,EAAE;QAAE,OAAO,WAAW,CAAC;IACpC,OAAO,eAAe,CAAC;AACzB,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** Severity levels for findings */
2
+ export type Severity = "critical" | "warning" | "info";
3
+ /** A single security finding */
4
+ export interface Finding {
5
+ rule: string;
6
+ severity: Severity;
7
+ file: string;
8
+ line?: number;
9
+ message: string;
10
+ evidence?: string;
11
+ }
12
+ /** Scan result for a directory */
13
+ export interface ScanResult {
14
+ target: string;
15
+ filesScanned: number;
16
+ linesScanned: number;
17
+ findings: Finding[];
18
+ score: number;
19
+ duration: number;
20
+ }
21
+ /** A scanner rule */
22
+ export interface Rule {
23
+ id: string;
24
+ name: string;
25
+ description: string;
26
+ run(files: ScannedFile[]): Finding[];
27
+ }
28
+ /** A file loaded for scanning */
29
+ export interface ScannedFile {
30
+ path: string;
31
+ relativePath: string;
32
+ content: string;
33
+ lines: string[];
34
+ ext: string;
35
+ }
36
+ /** Parsed SKILL.md metadata */
37
+ export interface SkillMetadata {
38
+ name?: string;
39
+ description?: string;
40
+ permissions?: string[];
41
+ [key: string]: unknown;
42
+ }
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,33 @@
1
+ {
2
+ "name": "@elliotllliu/agentshield",
3
+ "version": "0.1.0",
4
+ "description": "Security scanner for AI agent skills, MCP servers, and plugins",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentshield": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/cli.ts",
12
+ "test": "node --import tsx --test tests/**/*.test.ts",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": ["security", "ai-agent", "skill", "mcp", "scanner", "audit", "openclaw"],
16
+ "author": "Elliot Liu",
17
+ "license": "MIT",
18
+ "dependencies": {
19
+ "chalk": "^5.4.1",
20
+ "commander": "^13.1.0",
21
+ "glob": "^11.0.1",
22
+ "gray-matter": "^4.0.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.10.0",
26
+ "tsx": "^4.19.0",
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "files": ["dist", "README.md", "LICENSE"]
33
+ }