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 +1 -1
- package/src/cli.js +20 -8
- package/src/ignoreParser.js +108 -13
- package/src/interactivePrompt.js +15 -4
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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
|
-
|
|
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');
|
package/src/ignoreParser.js
CHANGED
|
@@ -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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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)
|
package/src/interactivePrompt.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
55
|
-
|
|
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');
|