ai-warden 0.4.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-warden",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AI security scanner - Detect prompt injection attacks before they reach production",
5
5
  "main": "src/scanner.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -126,13 +126,23 @@ async function scanCommand(targetPath, options = {}) {
126
126
  scannedCount++;
127
127
 
128
128
  if (!result.passed) {
129
- // Get ignored patterns for this file
130
- const ignoredPatterns = ignoreParser.getIgnoredPatterns(file);
129
+ // Get ignored patterns for this file (with hash support)
130
+ const ignoreInfo = ignoreParser.getIgnoredPatterns(file, content);
131
131
 
132
- // Filter out ignored patterns
133
- const activeFindings = result.findings.filter(f =>
134
- !ignoredPatterns.includes(f.id)
135
- );
132
+ // Filter out ignored patterns (regular + hash-based)
133
+ const activeFindings = result.findings.filter(f => {
134
+ // Check regular ignore list
135
+ if (ignoreInfo.patterns.includes(f.id)) {
136
+ return false;
137
+ }
138
+
139
+ // Check hash-based ignore (more secure)
140
+ if (ignoreParser.shouldIgnoreFindingWithHash(file, f, content)) {
141
+ return false;
142
+ }
143
+
144
+ return true;
145
+ });
136
146
 
137
147
  if (activeFindings.length === 0) {
138
148
  // All findings are ignored
@@ -148,13 +158,15 @@ async function scanCommand(targetPath, options = {}) {
148
158
 
149
159
  if (options.verbose && activeFindings.length > 0) {
150
160
  activeFindings.slice(0, 3).forEach(f => {
151
- console.log(` - ${f.severity}: ${f.description}`);
161
+ const name = f.name || f.id || 'Unknown';
162
+ const match = f.match ? ` ("${f.match.substring(0, 40)}...")` : '';
163
+ console.log(` - ${f.severity}: ${name}${match}`);
152
164
  });
153
165
  }
154
166
 
155
167
  // Interactive mode: prompt user
156
168
  if (interactivePrompt && activeFindings.length > 0) {
157
- const action = await interactivePrompt.promptThreat(file, activeFindings[0]);
169
+ const action = await interactivePrompt.promptThreat(file, activeFindings[0], content);
158
170
 
159
171
  if (action === 'quit') {
160
172
  console.log('\nšŸ›‘ Scan aborted by user\n');
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * .aiwardenignore Parser
3
3
  * Handles whitelist/ignore rules for files and patterns
4
+ * Supports hash-based verification for security
4
5
  */
5
6
 
6
7
  const fs = require('fs');
7
8
  const path = require('path');
9
+ const crypto = require('crypto');
8
10
 
9
11
  class IgnoreParser {
10
12
  constructor(ignoreFilePath = '.aiwardenignore') {
@@ -33,12 +35,25 @@ class IgnoreParser {
33
35
 
34
36
  // Check if it's a file:pattern rule
35
37
  if (trimmed.includes(':')) {
36
- const [filePath, patterns] = trimmed.split(':');
37
- this.rules.push({
38
- type: 'file-pattern',
39
- filePath: filePath.trim(),
40
- patterns: patterns.trim() === '*' ? '*' : patterns.trim().split(',').map(p => p.trim())
41
- });
38
+ const parts = trimmed.split(':');
39
+ const filePath = parts[0].trim();
40
+ const patterns = parts[1].trim();
41
+
42
+ // Check for hash-based rule: file:pattern:hash:abc123...
43
+ if (parts.length === 4 && parts[2] === 'hash') {
44
+ this.rules.push({
45
+ type: 'file-pattern-hash',
46
+ filePath,
47
+ patterns: patterns === '*' ? '*' : patterns.split(',').map(p => p.trim()),
48
+ hash: parts[3].trim()
49
+ });
50
+ } else {
51
+ this.rules.push({
52
+ type: 'file-pattern',
53
+ filePath,
54
+ patterns: patterns === '*' ? '*' : patterns.split(',').map(p => p.trim())
55
+ });
56
+ }
42
57
  } else {
43
58
  // It's a file/directory pattern
44
59
  this.rules.push({
@@ -72,21 +87,88 @@ class IgnoreParser {
72
87
  }
73
88
 
74
89
  /**
75
- * Get patterns to ignore for a specific file
90
+ * Get patterns to ignore for a specific file (with hash verification)
91
+ * Returns object: { patterns: [...], requiresHashCheck: [...] }
76
92
  */
77
- getIgnoredPatterns(filePath) {
93
+ getIgnoredPatterns(filePath, fileContent = null) {
78
94
  const normalized = path.normalize(filePath);
79
95
  const ignored = [];
96
+ const hashChecks = [];
80
97
 
81
98
  for (const rule of this.rules) {
82
99
  if (rule.type === 'file-pattern' && rule.patterns !== '*') {
83
100
  if (this.matchPath(normalized, rule.filePath)) {
84
101
  ignored.push(...rule.patterns);
85
102
  }
103
+ } else if (rule.type === 'file-pattern-hash') {
104
+ if (this.matchPath(normalized, rule.filePath)) {
105
+ // Hash-based rule requires content verification
106
+ hashChecks.push({
107
+ patterns: Array.isArray(rule.patterns) ? rule.patterns : [rule.patterns],
108
+ expectedHash: rule.hash
109
+ });
110
+ }
86
111
  }
87
112
  }
88
113
 
89
- return ignored;
114
+ return { patterns: ignored, hashChecks };
115
+ }
116
+
117
+ /**
118
+ * Verify if a finding should be ignored based on hash
119
+ */
120
+ shouldIgnoreFindingWithHash(filePath, finding, fileContent) {
121
+ const normalized = path.normalize(filePath);
122
+
123
+ for (const rule of this.rules) {
124
+ if (rule.type === 'file-pattern-hash' && this.matchPath(normalized, rule.filePath)) {
125
+ // Check if pattern matches
126
+ const patterns = Array.isArray(rule.patterns) ? rule.patterns : [rule.patterns];
127
+ if (patterns.includes(finding.id) || rule.patterns === '*') {
128
+ // Generate hash for this finding
129
+ const findingHash = this.generateFindingHash(filePath, finding, fileContent);
130
+
131
+ // Compare with stored hash
132
+ if (findingHash === rule.hash) {
133
+ return true; // Hash matches, ignore this finding
134
+ }
135
+ // Hash doesn't match = code changed = don't ignore!
136
+ }
137
+ }
138
+ }
139
+
140
+ return false;
141
+ }
142
+
143
+ /**
144
+ * Generate hash for a specific finding
145
+ * Hash includes: file path + pattern ID + matched content + surrounding context
146
+ */
147
+ generateFindingHash(filePath, finding, fileContent) {
148
+ // Create a unique fingerprint of this finding
149
+ const data = [
150
+ path.basename(filePath), // Just filename (not full path, more portable)
151
+ finding.id,
152
+ finding.match || '',
153
+ this.getContextAroundMatch(fileContent, finding.match, 50) // 50 chars context
154
+ ].join('::');
155
+
156
+ return crypto.createHash('sha256').update(data).digest('hex').substring(0, 16); // Short hash
157
+ }
158
+
159
+ /**
160
+ * Get surrounding context for a match (for more specific hash)
161
+ */
162
+ getContextAroundMatch(content, match, contextSize = 50) {
163
+ if (!content || !match) return '';
164
+
165
+ const index = content.indexOf(match);
166
+ if (index === -1) return '';
167
+
168
+ const start = Math.max(0, index - contextSize);
169
+ const end = Math.min(content.length, index + match.length + contextSize);
170
+
171
+ return content.substring(start, end);
90
172
  }
91
173
 
92
174
  /**
@@ -132,11 +214,24 @@ class IgnoreParser {
132
214
 
133
215
  /**
134
216
  * Add a new ignore rule and save to file
217
+ * @param {string} filePath - File to whitelist
218
+ * @param {string|array} patterns - Pattern IDs or '*'
219
+ * @param {object} options - { hash, finding, fileContent }
135
220
  */
136
- addIgnoreRule(filePath, patterns = '*') {
137
- const rule = patterns === '*'
138
- ? filePath
139
- : `${filePath}:${Array.isArray(patterns) ? patterns.join(',') : patterns}`;
221
+ addIgnoreRule(filePath, patterns = '*', options = {}) {
222
+ let rule;
223
+
224
+ if (options.hash && options.finding && options.fileContent) {
225
+ // Hash-based rule
226
+ const hash = this.generateFindingHash(filePath, options.finding, options.fileContent);
227
+ const patternStr = Array.isArray(patterns) ? patterns.join(',') : patterns;
228
+ rule = `${filePath}:${patternStr}:hash:${hash}`;
229
+ } else {
230
+ // Regular rule
231
+ rule = patterns === '*'
232
+ ? filePath
233
+ : `${filePath}:${Array.isArray(patterns) ? patterns.join(',') : patterns}`;
234
+ }
140
235
 
141
236
  // Check if rule already exists
142
237
  const content = fs.existsSync(this.ignoreFilePath)
@@ -13,8 +13,11 @@ class InteractivePrompt {
13
13
  /**
14
14
  * Prompt user what to do with a detected threat
15
15
  * Returns: 'ignore-file' | 'ignore-pattern' | 'keep' | 'quit'
16
+ * @param {string} filePath - File path
17
+ * @param {object} finding - Finding object
18
+ * @param {string} fileContent - File content (for hash generation)
16
19
  */
17
- async promptThreat(filePath, finding) {
20
+ async promptThreat(filePath, finding, fileContent = null) {
18
21
  return new Promise((resolve) => {
19
22
  const rl = readline.createInterface({
20
23
  input: process.stdin,
@@ -36,10 +39,12 @@ class InteractivePrompt {
36
39
 
37
40
  console.log('');
38
41
  console.log(' \x1b[32m[I]\x1b[0m Ignore this entire file');
39
- console.log(` \x1b[32m[P]\x1b[0m Ignore pattern ${finding.id} only`);
42
+ console.log(` \x1b[32m[P]\x1b[0m Ignore pattern ${finding.id} (hash-protected šŸ”’)`);
40
43
  console.log(' \x1b[32m[K]\x1b[0m Keep (this is a real threat)');
41
44
  console.log(' \x1b[32m[Q]\x1b[0m Quit scanning');
42
45
  console.log('');
46
+ console.log(' \x1b[90mšŸ’” Hash-protected = re-scanned if code changes\x1b[0m');
47
+ console.log('');
43
48
 
44
49
  rl.question(' Choice: ', (answer) => {
45
50
  rl.close();
@@ -51,8 +56,14 @@ class InteractivePrompt {
51
56
  console.log(` āœ… Added to .aiwardenignore: ${filePath}`);
52
57
  resolve('ignore-file');
53
58
  } else if (choice === 'p') {
54
- this.ignoreParser.addIgnoreRule(filePath, finding.id);
55
- console.log(` āœ… Added to .aiwardenignore: ${filePath}:${finding.id}`);
59
+ // Hash-protected whitelist
60
+ this.ignoreParser.addIgnoreRule(filePath, finding.id, {
61
+ hash: true,
62
+ finding,
63
+ fileContent
64
+ });
65
+ console.log(` āœ… Added to .aiwardenignore: ${filePath}:${finding.id} šŸ”’`);
66
+ console.log(` \x1b[90m (Hash-protected - will re-scan if code changes)\x1b[0m`);
56
67
  resolve('ignore-pattern');
57
68
  } else if (choice === 'k') {
58
69
  console.log(' āš ļø Kept as threat');