agent-threat-rules 0.1.0 → 0.2.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 (101) hide show
  1. package/README.md +358 -96
  2. package/dist/cli.js +90 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/coverage-analyzer.d.ts +43 -0
  5. package/dist/coverage-analyzer.d.ts.map +1 -0
  6. package/dist/coverage-analyzer.js +329 -0
  7. package/dist/coverage-analyzer.js.map +1 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-server.d.ts +13 -0
  13. package/dist/mcp-server.d.ts.map +1 -0
  14. package/dist/mcp-server.js +220 -0
  15. package/dist/mcp-server.js.map +1 -0
  16. package/dist/mcp-tools/coverage-gaps.d.ts +13 -0
  17. package/dist/mcp-tools/coverage-gaps.d.ts.map +1 -0
  18. package/dist/mcp-tools/coverage-gaps.js +55 -0
  19. package/dist/mcp-tools/coverage-gaps.js.map +1 -0
  20. package/dist/mcp-tools/list-rules.d.ts +17 -0
  21. package/dist/mcp-tools/list-rules.d.ts.map +1 -0
  22. package/dist/mcp-tools/list-rules.js +45 -0
  23. package/dist/mcp-tools/list-rules.js.map +1 -0
  24. package/dist/mcp-tools/scan.d.ts +18 -0
  25. package/dist/mcp-tools/scan.d.ts.map +1 -0
  26. package/dist/mcp-tools/scan.js +75 -0
  27. package/dist/mcp-tools/scan.js.map +1 -0
  28. package/dist/mcp-tools/submit-proposal.d.ts +12 -0
  29. package/dist/mcp-tools/submit-proposal.d.ts.map +1 -0
  30. package/dist/mcp-tools/submit-proposal.js +95 -0
  31. package/dist/mcp-tools/submit-proposal.js.map +1 -0
  32. package/dist/mcp-tools/threat-summary.d.ts +12 -0
  33. package/dist/mcp-tools/threat-summary.d.ts.map +1 -0
  34. package/dist/mcp-tools/threat-summary.js +74 -0
  35. package/dist/mcp-tools/threat-summary.js.map +1 -0
  36. package/dist/mcp-tools/validate.d.ts +15 -0
  37. package/dist/mcp-tools/validate.d.ts.map +1 -0
  38. package/dist/mcp-tools/validate.js +45 -0
  39. package/dist/mcp-tools/validate.js.map +1 -0
  40. package/dist/modules/index.d.ts +5 -4
  41. package/dist/modules/index.d.ts.map +1 -1
  42. package/dist/modules/index.js +6 -4
  43. package/dist/modules/index.js.map +1 -1
  44. package/dist/modules/semantic.d.ts +105 -0
  45. package/dist/modules/semantic.d.ts.map +1 -0
  46. package/dist/modules/semantic.js +283 -0
  47. package/dist/modules/semantic.js.map +1 -0
  48. package/dist/rule-scaffolder.d.ts +39 -0
  49. package/dist/rule-scaffolder.d.ts.map +1 -0
  50. package/dist/rule-scaffolder.js +173 -0
  51. package/dist/rule-scaffolder.js.map +1 -0
  52. package/dist/skill-fingerprint.d.ts +96 -0
  53. package/dist/skill-fingerprint.d.ts.map +1 -0
  54. package/dist/skill-fingerprint.js +337 -0
  55. package/dist/skill-fingerprint.js.map +1 -0
  56. package/dist/types.d.ts +1 -1
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +6 -1
  59. package/rules/agent-manipulation/ATR-2026-030-cross-agent-attack.yaml +1 -1
  60. package/rules/agent-manipulation/ATR-2026-032-goal-hijacking.yaml +1 -1
  61. package/rules/agent-manipulation/ATR-2026-074-cross-agent-privilege-escalation.yaml +1 -1
  62. package/rules/agent-manipulation/ATR-2026-076-inter-agent-message-spoofing.yaml +1 -1
  63. package/rules/agent-manipulation/ATR-2026-077-human-trust-exploitation.yaml +1 -1
  64. package/rules/context-exfiltration/ATR-2026-020-system-prompt-leak.yaml +1 -1
  65. package/rules/context-exfiltration/ATR-2026-021-api-key-exposure.yaml +1 -1
  66. package/rules/context-exfiltration/ATR-2026-075-agent-memory-manipulation.yaml +1 -1
  67. package/rules/data-poisoning/ATR-2026-070-data-poisoning.yaml +1 -1
  68. package/rules/excessive-autonomy/ATR-2026-050-runaway-agent-loop.yaml +1 -1
  69. package/rules/excessive-autonomy/ATR-2026-051-resource-exhaustion.yaml +1 -1
  70. package/rules/excessive-autonomy/ATR-2026-052-cascading-failure.yaml +1 -1
  71. package/rules/model-security/ATR-2026-072-model-behavior-extraction.yaml +1 -1
  72. package/rules/model-security/ATR-2026-073-malicious-finetuning-data.yaml +1 -1
  73. package/rules/privilege-escalation/ATR-2026-040-privilege-escalation.yaml +1 -1
  74. package/rules/privilege-escalation/ATR-2026-041-scope-creep.yaml +1 -1
  75. package/rules/prompt-injection/ATR-2026-001-direct-prompt-injection.yaml +1 -1
  76. package/rules/prompt-injection/ATR-2026-002-indirect-prompt-injection.yaml +1 -1
  77. package/rules/prompt-injection/ATR-2026-003-jailbreak-attempt.yaml +1 -1
  78. package/rules/prompt-injection/ATR-2026-004-system-prompt-override.yaml +1 -1
  79. package/rules/prompt-injection/ATR-2026-005-multi-turn-injection.yaml +1 -1
  80. package/rules/prompt-injection/ATR-2026-080-encoding-evasion.yaml +75 -0
  81. package/rules/prompt-injection/ATR-2026-081-semantic-multi-turn.yaml +72 -0
  82. package/rules/prompt-injection/ATR-2026-082-fingerprint-evasion.yaml +71 -0
  83. package/rules/prompt-injection/ATR-2026-083-indirect-tool-injection.yaml +71 -0
  84. package/rules/prompt-injection/ATR-2026-084-structured-data-injection.yaml +73 -0
  85. package/rules/prompt-injection/ATR-2026-085-audit-evasion.yaml +71 -0
  86. package/rules/prompt-injection/ATR-2026-086-visual-spoofing.yaml +75 -0
  87. package/rules/prompt-injection/ATR-2026-087-rule-probing.yaml +69 -0
  88. package/rules/prompt-injection/ATR-2026-088-adaptive-countermeasure.yaml +71 -0
  89. package/rules/prompt-injection/ATR-2026-089-polymorphic-skill.yaml +72 -0
  90. package/rules/prompt-injection/ATR-2026-090-threat-intel-exfil.yaml +71 -0
  91. package/rules/prompt-injection/ATR-2026-091-nested-payload.yaml +75 -0
  92. package/rules/prompt-injection/ATR-2026-092-consensus-poisoning.yaml +79 -0
  93. package/rules/prompt-injection/ATR-2026-093-gradual-escalation.yaml +73 -0
  94. package/rules/prompt-injection/ATR-2026-094-audit-bypass.yaml +73 -0
  95. package/rules/skill-compromise/ATR-2026-060-skill-impersonation.yaml +1 -1
  96. package/rules/tool-poisoning/ATR-2026-010-mcp-malicious-response.yaml +1 -1
  97. package/rules/tool-poisoning/ATR-2026-011-tool-output-injection.yaml +1 -1
  98. package/rules/tool-poisoning/ATR-2026-012-unauthorized-tool-call.yaml +1 -1
  99. package/rules/tool-poisoning/ATR-2026-013-tool-ssrf.yaml +1 -1
  100. package/rules/tool-poisoning/ATR-2026-095-supply-chain-poisoning.yaml +77 -0
  101. package/rules/tool-poisoning/ATR-2026-096-registry-poisoning.yaml +79 -0
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ATR Rule Scaffolder - Generates ATR rule YAML scaffolds from structured input
3
+ * @module agent-threat-rules/rule-scaffolder
4
+ */
5
+ import type { ATRCategory, ATRSeverity, ATRSourceType } from './types.js';
6
+ export interface ScaffoldInput {
7
+ title: string;
8
+ category: ATRCategory;
9
+ severity?: ATRSeverity;
10
+ attackDescription: string;
11
+ examplePayloads: string[];
12
+ agentSourceType?: ATRSourceType;
13
+ owaspRefs?: string[];
14
+ mitreRefs?: string[];
15
+ }
16
+ export interface ScaffoldResult {
17
+ yaml: string;
18
+ id: string;
19
+ warnings: string[];
20
+ }
21
+ export interface ScaffoldOptions {
22
+ author?: string;
23
+ schemaVersion?: string;
24
+ }
25
+ export declare class RuleScaffolder {
26
+ private readonly options;
27
+ constructor(options?: ScaffoldOptions);
28
+ /**
29
+ * Generate a complete ATR YAML rule from structured input.
30
+ * Returns a ScaffoldResult with the YAML string, generated ID, and any warnings.
31
+ */
32
+ scaffold(input: ScaffoldInput): ScaffoldResult;
33
+ /**
34
+ * Validate scaffold input, throwing on invalid required fields
35
+ * and returning warnings for non-critical issues.
36
+ */
37
+ private validateInput;
38
+ }
39
+ //# sourceMappingURL=rule-scaffolder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-scaffolder.d.ts","sourceRoot":"","sources":["../src/rule-scaffolder.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,aAAa,EAGd,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,CAAC,EAAE,aAAa,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAmED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAExC,OAAO,GAAE,eAAoB;IAOzC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,cAAc;IAwF9C;;;OAGG;IACH,OAAO,CAAC,aAAa;CAwBtB"}
@@ -0,0 +1,173 @@
1
+ /**
2
+ * ATR Rule Scaffolder - Generates ATR rule YAML scaffolds from structured input
3
+ * @module agent-threat-rules/rule-scaffolder
4
+ */
5
+ import yaml from 'js-yaml';
6
+ const CATEGORY_TO_SOURCE_TYPE = {
7
+ 'prompt-injection': 'llm_io',
8
+ 'tool-poisoning': 'tool_call',
9
+ 'context-exfiltration': 'context_window',
10
+ 'agent-manipulation': 'multi_agent_comm',
11
+ 'privilege-escalation': 'agent_behavior',
12
+ 'excessive-autonomy': 'agent_behavior',
13
+ 'data-poisoning': 'llm_io',
14
+ 'model-abuse': 'llm_io',
15
+ 'skill-compromise': 'skill_lifecycle',
16
+ };
17
+ const CATEGORY_TO_FIELD = {
18
+ 'prompt-injection': 'user_input',
19
+ 'tool-poisoning': 'tool_response',
20
+ 'context-exfiltration': 'agent_output',
21
+ 'agent-manipulation': 'agent_message',
22
+ 'privilege-escalation': 'agent_action',
23
+ 'excessive-autonomy': 'agent_action',
24
+ 'data-poisoning': 'training_input',
25
+ 'model-abuse': 'user_input',
26
+ 'skill-compromise': 'skill_manifest',
27
+ };
28
+ const SEVERITY_TO_ACTIONS = {
29
+ critical: ['block_input', 'alert', 'escalate'],
30
+ high: ['block_input', 'alert'],
31
+ medium: ['alert', 'snapshot'],
32
+ low: ['alert'],
33
+ informational: ['alert'],
34
+ };
35
+ const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g;
36
+ function escapeRegex(str) {
37
+ return str.replace(REGEX_SPECIAL_CHARS, '\\$&');
38
+ }
39
+ /**
40
+ * Build a case-insensitive regex pattern from a payload string.
41
+ * Extracts significant keywords (length > 3) and creates lookahead assertions,
42
+ * falling back to a simple escaped match for short payloads.
43
+ */
44
+ function buildRegexPattern(payload) {
45
+ const trimmed = payload.trim();
46
+ const words = trimmed.split(/\s+/).filter((w) => w.length > 3);
47
+ if (words.length === 0) {
48
+ return `(?i).*${escapeRegex(trimmed)}.*`;
49
+ }
50
+ const keywords = words.slice(0, 4);
51
+ return `(?i)${keywords.map((k) => `(?=.*${escapeRegex(k)})`).join('')}`;
52
+ }
53
+ function generateId() {
54
+ const year = new Date().getFullYear();
55
+ const seq = String(Math.floor(Math.random() * 900) + 100);
56
+ return `ATR-${year}-${seq}`;
57
+ }
58
+ function getCurrentDate() {
59
+ return new Date().toISOString().split('T')[0];
60
+ }
61
+ export class RuleScaffolder {
62
+ options;
63
+ constructor(options = {}) {
64
+ this.options = {
65
+ author: options.author ?? 'ATR Community (auto-scaffolded)',
66
+ schemaVersion: options.schemaVersion ?? '0.1',
67
+ };
68
+ }
69
+ /**
70
+ * Generate a complete ATR YAML rule from structured input.
71
+ * Returns a ScaffoldResult with the YAML string, generated ID, and any warnings.
72
+ */
73
+ scaffold(input) {
74
+ const warnings = this.validateInput(input);
75
+ const severity = input.severity ?? 'medium';
76
+ const sourceType = input.agentSourceType ?? CATEGORY_TO_SOURCE_TYPE[input.category];
77
+ const field = CATEGORY_TO_FIELD[input.category];
78
+ const id = generateId();
79
+ const date = getCurrentDate();
80
+ const conditions = input.examplePayloads.map((payload, idx) => ({
81
+ field,
82
+ operator: 'regex',
83
+ value: buildRegexPattern(payload),
84
+ description: `Pattern ${idx + 1}: detects "${payload.trim()}"`,
85
+ }));
86
+ const truePositives = input.examplePayloads.map((payload) => ({
87
+ input: payload.trim(),
88
+ expected: 'trigger',
89
+ }));
90
+ const trueNegatives = [
91
+ {
92
+ input: 'TODO: Add benign input that should not trigger this rule',
93
+ expected: 'no_trigger',
94
+ },
95
+ ];
96
+ const references = {};
97
+ if (input.owaspRefs && input.owaspRefs.length > 0) {
98
+ references.owasp_llm = [...input.owaspRefs];
99
+ }
100
+ if (input.mitreRefs && input.mitreRefs.length > 0) {
101
+ references.mitre_atlas = [...input.mitreRefs];
102
+ }
103
+ const conditionExpr = conditions.length > 1 ? 'any' : 'all';
104
+ const rule = {
105
+ title: input.title,
106
+ id,
107
+ schema_version: this.options.schemaVersion,
108
+ status: 'draft',
109
+ description: input.attackDescription,
110
+ author: this.options.author,
111
+ date,
112
+ severity,
113
+ detection_tier: 'pattern',
114
+ maturity: 'draft',
115
+ ...(Object.keys(references).length > 0 ? { references } : {}),
116
+ tags: {
117
+ category: input.category,
118
+ confidence: severity === 'critical' || severity === 'high' ? 'high' : 'medium',
119
+ },
120
+ agent_source: {
121
+ type: sourceType,
122
+ },
123
+ detection: {
124
+ conditions,
125
+ condition: conditionExpr,
126
+ false_positives: [
127
+ 'TODO: Document known false positive scenarios',
128
+ ],
129
+ },
130
+ response: {
131
+ actions: [...SEVERITY_TO_ACTIONS[severity]],
132
+ message_template: `Potential ${input.category} detected: {{matched_patterns}}`,
133
+ },
134
+ test_cases: {
135
+ true_positives: truePositives,
136
+ true_negatives: trueNegatives,
137
+ },
138
+ };
139
+ const yamlStr = yaml.dump(rule, {
140
+ indent: 2,
141
+ lineWidth: 120,
142
+ noRefs: true,
143
+ sortKeys: false,
144
+ quotingType: '"',
145
+ forceQuotes: false,
146
+ });
147
+ return { yaml: yamlStr, id, warnings };
148
+ }
149
+ /**
150
+ * Validate scaffold input, throwing on invalid required fields
151
+ * and returning warnings for non-critical issues.
152
+ */
153
+ validateInput(input) {
154
+ const warnings = [];
155
+ if (!input.title || input.title.trim().length === 0) {
156
+ throw new Error('ScaffoldInput.title is required and must be non-empty');
157
+ }
158
+ if (!input.category) {
159
+ throw new Error('ScaffoldInput.category is required');
160
+ }
161
+ if (!input.attackDescription || input.attackDescription.trim().length === 0) {
162
+ throw new Error('ScaffoldInput.attackDescription is required and must be non-empty');
163
+ }
164
+ if (!input.examplePayloads || input.examplePayloads.length === 0) {
165
+ throw new Error('ScaffoldInput.examplePayloads must contain at least one payload');
166
+ }
167
+ if (input.examplePayloads.length < 3) {
168
+ warnings.push('Fewer than 3 example payloads - consider adding more for better pattern coverage.');
169
+ }
170
+ return warnings;
171
+ }
172
+ }
173
+ //# sourceMappingURL=rule-scaffolder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-scaffolder.js","sourceRoot":"","sources":["../src/rule-scaffolder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Skill Behavioral Fingerprint
3
+ * Skill 行為指紋追蹤器
4
+ *
5
+ * Tracks what each skill "normally does" across invocations, then detects
6
+ * behavioral drift when a previously-trusted skill starts acting differently.
7
+ *
8
+ * Solves the "installed then turns malicious" scenario:
9
+ * - First N invocations: build fingerprint (what APIs, what patterns, what scope)
10
+ * - After fingerprint stabilizes: flag any deviation as anomaly
11
+ *
12
+ * 追蹤每個 skill 的「正常行為」,然後在行為偏移時偵測:
13
+ * - 前 N 次呼叫:建立指紋
14
+ * - 指紋穩定後:任何偏離都標記為異常
15
+ *
16
+ * @module agent-threat-rules/skill-fingerprint
17
+ */
18
+ import type { AgentEvent } from './types.js';
19
+ /** Behavioral capabilities observed for a skill */
20
+ interface SkillCapabilities {
21
+ /** Seen filesystem operations (read/write/delete) */
22
+ readonly filesystemOps: ReadonlySet<string>;
23
+ /** Seen network destinations (hostnames) */
24
+ readonly networkTargets: ReadonlySet<string>;
25
+ /** Seen environment variable accesses */
26
+ readonly envAccesses: ReadonlySet<string>;
27
+ /** Seen child process executions */
28
+ readonly processExecs: ReadonlySet<string>;
29
+ /** Seen output patterns (categories: data, error, redirect, exfiltration) */
30
+ readonly outputPatterns: ReadonlySet<string>;
31
+ }
32
+ /** Immutable fingerprint snapshot */
33
+ export interface SkillFingerprint {
34
+ readonly skillName: string;
35
+ readonly invocationCount: number;
36
+ readonly firstSeen: number;
37
+ readonly lastSeen: number;
38
+ readonly isStable: boolean;
39
+ readonly capabilities: SkillCapabilities;
40
+ /** Hash of capabilities for quick comparison */
41
+ readonly capabilityHash: string;
42
+ }
43
+ /** Anomaly when behavior deviates from fingerprint */
44
+ export interface BehaviorAnomaly {
45
+ readonly skillName: string;
46
+ readonly anomalyType: 'new_filesystem_op' | 'new_network_target' | 'new_env_access' | 'new_process_exec' | 'new_output_pattern' | 'capability_expansion';
47
+ readonly description: string;
48
+ readonly severity: 'low' | 'medium' | 'high' | 'critical';
49
+ readonly newValue: string;
50
+ readonly timestamp: number;
51
+ }
52
+ export interface SkillFingerprintConfig {
53
+ /** Minimum invocations before fingerprint can stabilize (default: 10) */
54
+ stabilityThreshold?: number;
55
+ /** Consecutive clean invocations to mark stable (default: 5) */
56
+ stableStreak?: number;
57
+ }
58
+ export declare class SkillFingerprintStore {
59
+ private readonly fingerprints;
60
+ private readonly stabilityThreshold;
61
+ private readonly stableStreak;
62
+ constructor(config?: SkillFingerprintConfig);
63
+ /**
64
+ * Record a skill invocation and detect behavioral anomalies.
65
+ * Returns anomalies if the fingerprint was stable and new capabilities appeared.
66
+ *
67
+ * 記錄 skill 呼叫並偵測行為異常。
68
+ * 如果指紋已穩定且出現新能力,回傳異常列表。
69
+ */
70
+ recordInvocation(skillName: string, event: AgentEvent): readonly BehaviorAnomaly[];
71
+ /**
72
+ * Get an immutable fingerprint snapshot for a skill.
73
+ * 取得某 skill 的不可變指紋快照。
74
+ */
75
+ getFingerprint(skillName: string): SkillFingerprint | undefined;
76
+ /** Get all tracked skill names */
77
+ getTrackedSkills(): string[];
78
+ /** Get count of stable fingerprints */
79
+ getStableCount(): number;
80
+ /** Get total tracked skills */
81
+ getTrackedCount(): number;
82
+ /**
83
+ * Reset a skill's fingerprint (e.g., after a legitimate update).
84
+ * 重置 skill 指紋(例如合法更新後)。
85
+ */
86
+ resetFingerprint(skillName: string): void;
87
+ /**
88
+ * Evict fingerprints not seen since cutoffMs ago.
89
+ * 清除超過 cutoffMs 未活動的指紋。
90
+ */
91
+ cleanup(cutoffMs: number): number;
92
+ private getOrCreate;
93
+ private computeCapabilityHash;
94
+ }
95
+ export {};
96
+ //# sourceMappingURL=skill-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-fingerprint.d.ts","sourceRoot":"","sources":["../src/skill-fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,mDAAmD;AACnD,UAAU,iBAAiB;IACzB,qDAAqD;IACrD,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC5C,4CAA4C;IAC5C,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,yCAAyC;IACzC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,oCAAoC;IACpC,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,6EAA6E;IAC7E,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAC9C;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,iBAAiB,CAAC;IACzC,gDAAgD;IAChD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,sDAAsD;AACtD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAChB,mBAAmB,GACnB,oBAAoB,GACpB,gBAAgB,GAChB,kBAAkB,GAClB,oBAAoB,GACpB,sBAAsB,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAqGD,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyC;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,MAAM,CAAC,EAAE,sBAAsB;IAK3C;;;;;;OAMG;IACH,gBAAgB,CACd,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,UAAU,GAChB,SAAS,eAAe,EAAE;IA8H7B;;;OAGG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAqB/D,kCAAkC;IAClC,gBAAgB,IAAI,MAAM,EAAE;IAI5B,uCAAuC;IACvC,cAAc,IAAI,MAAM;IAQxB,+BAA+B;IAC/B,eAAe,IAAI,MAAM;IAIzB;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIzC;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAgBjC,OAAO,CAAC,WAAW;IAkCnB,OAAO,CAAC,qBAAqB;CAU9B"}
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Skill Behavioral Fingerprint
3
+ * Skill 行為指紋追蹤器
4
+ *
5
+ * Tracks what each skill "normally does" across invocations, then detects
6
+ * behavioral drift when a previously-trusted skill starts acting differently.
7
+ *
8
+ * Solves the "installed then turns malicious" scenario:
9
+ * - First N invocations: build fingerprint (what APIs, what patterns, what scope)
10
+ * - After fingerprint stabilizes: flag any deviation as anomaly
11
+ *
12
+ * 追蹤每個 skill 的「正常行為」,然後在行為偏移時偵測:
13
+ * - 前 N 次呼叫:建立指紋
14
+ * - 指紋穩定後:任何偏離都標記為異常
15
+ *
16
+ * @module agent-threat-rules/skill-fingerprint
17
+ */
18
+ import { createHash } from 'node:crypto';
19
+ // ---------------------------------------------------------------------------
20
+ // Pattern detectors (regex-based, no LLM needed)
21
+ // ---------------------------------------------------------------------------
22
+ const FS_WRITE_PATTERN = /(?:write(?:File)?|appendFile|fs\.write|truncate|mkdir|rmdir|unlink|rm\s+-)/i;
23
+ const FS_READ_PATTERN = /(?:read(?:File)?|readdir|stat|access|exists|glob|find\s)/i;
24
+ const FS_DELETE_PATTERN = /(?:unlink|rm\s+-rf|delete(?:File)?|removeDir|rmdir)/i;
25
+ const NETWORK_PATTERN = /(?:https?:\/\/|fetch|curl|wget|axios|http\.request|net\.connect|socket)[\s('"]*([a-zA-Z0-9.-]+(?:\.[a-zA-Z]{2,}))/i;
26
+ const ENV_PATTERN = /(?:process\.env|os\.environ|getenv|System\.getenv)\[?['"(]?([A-Z_][A-Z0-9_]*)/i;
27
+ const ENV_INLINE_PATTERN = /\$\{?([A-Z_][A-Z0-9_]{2,})\}?/g;
28
+ const EXEC_PATTERN = /(?:child_process|spawn|exec(?:File)?|system\(|popen|subprocess|shell_exec|os\.system)\s*\(\s*['"(]?([^\s'")\]]{1,80})/i;
29
+ const EXFIL_PATTERN = /(?:base64|btoa|encode|compress|deflate|gzip).*(?:http|fetch|curl|send|post|upload)/i;
30
+ const REDIRECT_PATTERN = /(?:redirect|forward|proxy|tunnel)\s+(?:to\s+)?(?:https?:\/\/)/i;
31
+ /** Classify a text content into behavioral capabilities */
32
+ function extractCapabilities(text) {
33
+ const result = {
34
+ filesystemOps: [],
35
+ networkTargets: [],
36
+ envAccesses: [],
37
+ processExecs: [],
38
+ outputPatterns: [],
39
+ };
40
+ if (!text || text.length === 0)
41
+ return result;
42
+ // Limit analysis to first 10KB to prevent ReDoS
43
+ const safeText = text.slice(0, 10_240);
44
+ // Filesystem
45
+ if (FS_WRITE_PATTERN.test(safeText))
46
+ result.filesystemOps.push('write');
47
+ if (FS_READ_PATTERN.test(safeText))
48
+ result.filesystemOps.push('read');
49
+ if (FS_DELETE_PATTERN.test(safeText))
50
+ result.filesystemOps.push('delete');
51
+ // Network targets
52
+ const netMatch = safeText.match(NETWORK_PATTERN);
53
+ if (netMatch?.[1])
54
+ result.networkTargets.push(netMatch[1]);
55
+ // Environment variable accesses
56
+ const envMatch = safeText.match(ENV_PATTERN);
57
+ if (envMatch?.[1])
58
+ result.envAccesses.push(envMatch[1]);
59
+ // Also check inline env vars like $HOME, ${API_KEY}
60
+ for (const m of safeText.matchAll(ENV_INLINE_PATTERN)) {
61
+ if (m[1] && !result.envAccesses.includes(m[1])) {
62
+ result.envAccesses.push(m[1]);
63
+ }
64
+ }
65
+ // Process executions
66
+ const execMatch = safeText.match(EXEC_PATTERN);
67
+ if (execMatch?.[1])
68
+ result.processExecs.push(execMatch[1]);
69
+ // Output patterns
70
+ if (EXFIL_PATTERN.test(safeText))
71
+ result.outputPatterns.push('exfiltration');
72
+ if (REDIRECT_PATTERN.test(safeText))
73
+ result.outputPatterns.push('redirect');
74
+ return result;
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Fingerprint Store
78
+ // ---------------------------------------------------------------------------
79
+ /** Default invocations needed before fingerprint is considered stable */
80
+ const DEFAULT_STABILITY_THRESHOLD = 10;
81
+ /** Consecutive invocations with no new capabilities to mark stable */
82
+ const DEFAULT_STABLE_STREAK = 5;
83
+ /** Maximum number of skills to track */
84
+ const MAX_SKILLS = 5_000;
85
+ export class SkillFingerprintStore {
86
+ fingerprints = new Map();
87
+ stabilityThreshold;
88
+ stableStreak;
89
+ constructor(config) {
90
+ this.stabilityThreshold = config?.stabilityThreshold ?? DEFAULT_STABILITY_THRESHOLD;
91
+ this.stableStreak = config?.stableStreak ?? DEFAULT_STABLE_STREAK;
92
+ }
93
+ /**
94
+ * Record a skill invocation and detect behavioral anomalies.
95
+ * Returns anomalies if the fingerprint was stable and new capabilities appeared.
96
+ *
97
+ * 記錄 skill 呼叫並偵測行為異常。
98
+ * 如果指紋已穩定且出現新能力,回傳異常列表。
99
+ */
100
+ recordInvocation(skillName, event) {
101
+ const now = Date.now();
102
+ const fp = this.getOrCreate(skillName, now);
103
+ fp.invocationCount++;
104
+ fp.lastSeen = now;
105
+ // Extract capabilities from event content + fields
106
+ const content = [
107
+ event.content ?? '',
108
+ event.fields?.['tool_args'] ?? '',
109
+ event.fields?.['tool_response'] ?? '',
110
+ ].join('\n');
111
+ const caps = extractCapabilities(content);
112
+ // Check for anomalies (only if fingerprint is stable)
113
+ const anomalies = [];
114
+ const isStable = fp.stableHash !== null;
115
+ if (isStable) {
116
+ // Detect NEW capabilities not in the stable fingerprint
117
+ for (const op of caps.filesystemOps) {
118
+ if (!fp.filesystemOps.has(op)) {
119
+ anomalies.push({
120
+ skillName,
121
+ anomalyType: 'new_filesystem_op',
122
+ description: `Skill "${skillName}" performing new filesystem operation: ${op} (not in baseline)`,
123
+ severity: op === 'delete' ? 'critical' : op === 'write' ? 'high' : 'medium',
124
+ newValue: op,
125
+ timestamp: now,
126
+ });
127
+ }
128
+ }
129
+ for (const target of caps.networkTargets) {
130
+ if (!fp.networkTargets.has(target)) {
131
+ anomalies.push({
132
+ skillName,
133
+ anomalyType: 'new_network_target',
134
+ description: `Skill "${skillName}" contacting new network target: ${target}`,
135
+ severity: 'high',
136
+ newValue: target,
137
+ timestamp: now,
138
+ });
139
+ }
140
+ }
141
+ for (const env of caps.envAccesses) {
142
+ if (!fp.envAccesses.has(env)) {
143
+ const isSensitive = /(?:KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)/i.test(env);
144
+ anomalies.push({
145
+ skillName,
146
+ anomalyType: 'new_env_access',
147
+ description: `Skill "${skillName}" accessing new env var: ${env}`,
148
+ severity: isSensitive ? 'critical' : 'medium',
149
+ newValue: env,
150
+ timestamp: now,
151
+ });
152
+ }
153
+ }
154
+ for (const proc of caps.processExecs) {
155
+ if (!fp.processExecs.has(proc)) {
156
+ anomalies.push({
157
+ skillName,
158
+ anomalyType: 'new_process_exec',
159
+ description: `Skill "${skillName}" executing new process: ${proc}`,
160
+ severity: 'critical',
161
+ newValue: proc,
162
+ timestamp: now,
163
+ });
164
+ }
165
+ }
166
+ for (const pat of caps.outputPatterns) {
167
+ if (!fp.outputPatterns.has(pat)) {
168
+ anomalies.push({
169
+ skillName,
170
+ anomalyType: 'new_output_pattern',
171
+ description: `Skill "${skillName}" exhibiting new pattern: ${pat}`,
172
+ severity: pat === 'exfiltration' ? 'critical' : 'high',
173
+ newValue: pat,
174
+ timestamp: now,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ // Update fingerprint with observed capabilities
180
+ let newCapsSeen = false;
181
+ for (const op of caps.filesystemOps) {
182
+ if (!fp.filesystemOps.has(op)) {
183
+ fp.filesystemOps.add(op);
184
+ newCapsSeen = true;
185
+ }
186
+ }
187
+ for (const t of caps.networkTargets) {
188
+ if (!fp.networkTargets.has(t)) {
189
+ fp.networkTargets.add(t);
190
+ newCapsSeen = true;
191
+ }
192
+ }
193
+ for (const e of caps.envAccesses) {
194
+ if (!fp.envAccesses.has(e)) {
195
+ fp.envAccesses.add(e);
196
+ newCapsSeen = true;
197
+ }
198
+ }
199
+ for (const p of caps.processExecs) {
200
+ if (!fp.processExecs.has(p)) {
201
+ fp.processExecs.add(p);
202
+ newCapsSeen = true;
203
+ }
204
+ }
205
+ for (const o of caps.outputPatterns) {
206
+ if (!fp.outputPatterns.has(o)) {
207
+ fp.outputPatterns.add(o);
208
+ newCapsSeen = true;
209
+ }
210
+ }
211
+ // Track stability
212
+ if (!isStable) {
213
+ if (newCapsSeen) {
214
+ fp.stableStreak = 0;
215
+ }
216
+ else {
217
+ fp.stableStreak++;
218
+ }
219
+ // Mark stable when threshold met
220
+ if (fp.invocationCount >= this.stabilityThreshold &&
221
+ fp.stableStreak >= this.stableStreak) {
222
+ fp.stableHash = this.computeCapabilityHash(fp);
223
+ }
224
+ }
225
+ return anomalies;
226
+ }
227
+ /**
228
+ * Get an immutable fingerprint snapshot for a skill.
229
+ * 取得某 skill 的不可變指紋快照。
230
+ */
231
+ getFingerprint(skillName) {
232
+ const fp = this.fingerprints.get(skillName);
233
+ if (!fp)
234
+ return undefined;
235
+ return {
236
+ skillName: fp.skillName,
237
+ invocationCount: fp.invocationCount,
238
+ firstSeen: fp.firstSeen,
239
+ lastSeen: fp.lastSeen,
240
+ isStable: fp.stableHash !== null,
241
+ capabilities: {
242
+ filesystemOps: new Set(fp.filesystemOps),
243
+ networkTargets: new Set(fp.networkTargets),
244
+ envAccesses: new Set(fp.envAccesses),
245
+ processExecs: new Set(fp.processExecs),
246
+ outputPatterns: new Set(fp.outputPatterns),
247
+ },
248
+ capabilityHash: fp.stableHash ?? this.computeCapabilityHash(fp),
249
+ };
250
+ }
251
+ /** Get all tracked skill names */
252
+ getTrackedSkills() {
253
+ return [...this.fingerprints.keys()];
254
+ }
255
+ /** Get count of stable fingerprints */
256
+ getStableCount() {
257
+ let count = 0;
258
+ for (const fp of this.fingerprints.values()) {
259
+ if (fp.stableHash !== null)
260
+ count++;
261
+ }
262
+ return count;
263
+ }
264
+ /** Get total tracked skills */
265
+ getTrackedCount() {
266
+ return this.fingerprints.size;
267
+ }
268
+ /**
269
+ * Reset a skill's fingerprint (e.g., after a legitimate update).
270
+ * 重置 skill 指紋(例如合法更新後)。
271
+ */
272
+ resetFingerprint(skillName) {
273
+ this.fingerprints.delete(skillName);
274
+ }
275
+ /**
276
+ * Evict fingerprints not seen since cutoffMs ago.
277
+ * 清除超過 cutoffMs 未活動的指紋。
278
+ */
279
+ cleanup(cutoffMs) {
280
+ const cutoff = Date.now() - cutoffMs;
281
+ let evicted = 0;
282
+ for (const [name, fp] of this.fingerprints) {
283
+ if (fp.lastSeen < cutoff) {
284
+ this.fingerprints.delete(name);
285
+ evicted++;
286
+ }
287
+ }
288
+ return evicted;
289
+ }
290
+ // -----------------------------------------------------------------------
291
+ // Private
292
+ // -----------------------------------------------------------------------
293
+ getOrCreate(skillName, now) {
294
+ const existing = this.fingerprints.get(skillName);
295
+ if (existing)
296
+ return existing;
297
+ // Evict oldest if at capacity
298
+ if (this.fingerprints.size >= MAX_SKILLS) {
299
+ let oldestName;
300
+ let oldestTime = Infinity;
301
+ for (const [name, fp] of this.fingerprints) {
302
+ if (fp.lastSeen < oldestTime) {
303
+ oldestTime = fp.lastSeen;
304
+ oldestName = name;
305
+ }
306
+ }
307
+ if (oldestName)
308
+ this.fingerprints.delete(oldestName);
309
+ }
310
+ const fp = {
311
+ skillName,
312
+ invocationCount: 0,
313
+ firstSeen: now,
314
+ lastSeen: now,
315
+ filesystemOps: new Set(),
316
+ networkTargets: new Set(),
317
+ envAccesses: new Set(),
318
+ processExecs: new Set(),
319
+ outputPatterns: new Set(),
320
+ stableHash: null,
321
+ stableStreak: 0,
322
+ };
323
+ this.fingerprints.set(skillName, fp);
324
+ return fp;
325
+ }
326
+ computeCapabilityHash(fp) {
327
+ const parts = [
328
+ [...fp.filesystemOps].sort().join(','),
329
+ [...fp.networkTargets].sort().join(','),
330
+ [...fp.envAccesses].sort().join(','),
331
+ [...fp.processExecs].sort().join(','),
332
+ [...fp.outputPatterns].sort().join(','),
333
+ ];
334
+ return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
335
+ }
336
+ }
337
+ //# sourceMappingURL=skill-fingerprint.js.map