ai-warden 0.3.0 → 0.4.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 +67 -0
- package/package.json +1 -1
- package/src/cli.js +65 -10
- package/src/fileClassifier.js +1 -1
- package/src/ignoreParser.js +163 -0
- package/src/interactivePrompt.js +89 -0
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
AI-Warden is a fast, zero-dependency security scanner that detects prompt injection vulnerabilities in your AI/LLM applications.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/ai-warden)
|
|
8
|
+
[](https://github.com/larhog/ai-warden-dev/actions/workflows/test.yml)
|
|
9
|
+
[](https://github.com/larhog/ai-warden-dev/actions/workflows/security-scan.yml)
|
|
8
10
|
[](LICENSE)
|
|
9
11
|
|
|
10
12
|
---
|
|
@@ -73,6 +75,9 @@ aiwarden scan . --mode permissive # Less sensitive (threshold: 250)
|
|
|
73
75
|
# Verbose output
|
|
74
76
|
aiwarden scan . --verbose
|
|
75
77
|
|
|
78
|
+
# Interactive mode (whitelist threats as you go)
|
|
79
|
+
aiwarden scan . --interactive
|
|
80
|
+
|
|
76
81
|
# Show version
|
|
77
82
|
aiwarden version
|
|
78
83
|
|
|
@@ -155,6 +160,68 @@ jobs:
|
|
|
155
160
|
|
|
156
161
|
---
|
|
157
162
|
|
|
163
|
+
## 🚫 Whitelist / Ignore Files
|
|
164
|
+
|
|
165
|
+
### Interactive Mode (Recommended)
|
|
166
|
+
|
|
167
|
+
When scanning, use `--interactive` to whitelist false positives on-the-fly:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
aiwarden scan . --interactive
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Example workflow:**
|
|
174
|
+
```
|
|
175
|
+
⚠️ Threat detected:
|
|
176
|
+
File: src/examples.js
|
|
177
|
+
Pattern: P001 - Ignore Previous Instructions
|
|
178
|
+
Risk: CRITICAL (Score: 450)
|
|
179
|
+
Found: "Ignore all previous instructions..."
|
|
180
|
+
|
|
181
|
+
[I] Ignore this entire file
|
|
182
|
+
[P] Ignore pattern P001 only
|
|
183
|
+
[K] Keep (this is a real threat)
|
|
184
|
+
[Q] Quit scanning
|
|
185
|
+
|
|
186
|
+
Choice: i
|
|
187
|
+
✅ Added to .aiwardenignore: src/examples.js
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Manual `.aiwardenignore` File
|
|
191
|
+
|
|
192
|
+
Create a `.aiwardenignore` file in your project root:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# .aiwardenignore
|
|
196
|
+
|
|
197
|
+
# Ignore entire directories
|
|
198
|
+
docs/
|
|
199
|
+
tests/
|
|
200
|
+
examples/
|
|
201
|
+
|
|
202
|
+
# Ignore specific files
|
|
203
|
+
src/patterns.js
|
|
204
|
+
src/securityTraining.js
|
|
205
|
+
|
|
206
|
+
# Wildcard patterns
|
|
207
|
+
**/*.test.js
|
|
208
|
+
**/*.spec.js
|
|
209
|
+
*.md
|
|
210
|
+
|
|
211
|
+
# Ignore specific patterns in files
|
|
212
|
+
src/config.js:P001,P002 # Only ignore these pattern IDs
|
|
213
|
+
src/examples.js:* # Ignore all patterns in this file
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Supported formats:**
|
|
217
|
+
- `path/to/file` - Ignore entire file
|
|
218
|
+
- `directory/` - Ignore entire directory
|
|
219
|
+
- `**/*.ext` - Wildcard patterns
|
|
220
|
+
- `file.js:P001,P002` - Ignore specific pattern IDs
|
|
221
|
+
- `file.js:*` - Ignore all patterns in file
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
158
225
|
## 📊 Example Output
|
|
159
226
|
|
|
160
227
|
```
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const scanner = require('./scanner');
|
|
11
11
|
const fileClassifier = require('./fileClassifier');
|
|
12
|
+
const IgnoreParser = require('./ignoreParser');
|
|
13
|
+
const InteractivePrompt = require('./interactivePrompt');
|
|
12
14
|
|
|
13
15
|
const args = process.argv.slice(2);
|
|
14
16
|
const command = args[0];
|
|
@@ -31,10 +33,12 @@ Commands:
|
|
|
31
33
|
Options:
|
|
32
34
|
--mode <mode> Detection mode (strict|balanced|permissive)
|
|
33
35
|
--verbose Show detailed output
|
|
36
|
+
--interactive Interactive mode (whitelist threats as you go)
|
|
34
37
|
|
|
35
38
|
Examples:
|
|
36
39
|
aiwarden scan . Scan current directory
|
|
37
40
|
aiwarden scan file.txt Scan single file
|
|
41
|
+
aiwarden scan . --interactive Interactive whitelist mode
|
|
38
42
|
aiwarden scan . --mode strict Strict detection mode
|
|
39
43
|
aiwarden scan . --verbose Detailed output
|
|
40
44
|
|
|
@@ -49,7 +53,7 @@ function showVersion() {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// Scan command
|
|
52
|
-
function scanCommand(targetPath, options = {}) {
|
|
56
|
+
async function scanCommand(targetPath, options = {}) {
|
|
53
57
|
if (!targetPath) {
|
|
54
58
|
console.error('❌ Error: No path specified');
|
|
55
59
|
console.log('Usage: aiwarden scan <path>');
|
|
@@ -63,6 +67,10 @@ function scanCommand(targetPath, options = {}) {
|
|
|
63
67
|
process.exit(1);
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
// Initialize ignore parser
|
|
71
|
+
const ignoreParser = new IgnoreParser();
|
|
72
|
+
const interactivePrompt = options.interactive ? new InteractivePrompt(ignoreParser) : null;
|
|
73
|
+
|
|
66
74
|
console.log(`🔍 AI-Warden scanning: ${fullPath}\n`);
|
|
67
75
|
|
|
68
76
|
const stat = fs.statSync(fullPath);
|
|
@@ -80,15 +88,27 @@ function scanCommand(targetPath, options = {}) {
|
|
|
80
88
|
|
|
81
89
|
let totalThreats = 0;
|
|
82
90
|
let scannedCount = 0;
|
|
91
|
+
let ignoredCount = 0;
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
for (const file of files) {
|
|
85
94
|
try {
|
|
95
|
+
const relativePath = path.relative(process.cwd(), file);
|
|
96
|
+
|
|
86
97
|
// Check if file should be skipped
|
|
87
98
|
if (fileClassifier.shouldSkipFile(file)) {
|
|
88
99
|
if (options.verbose) {
|
|
89
|
-
console.log(`⏭️ ${
|
|
100
|
+
console.log(`⏭️ ${relativePath} - Skipped`);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if file is in ignore list
|
|
106
|
+
if (ignoreParser.shouldIgnoreFile(file)) {
|
|
107
|
+
ignoredCount++;
|
|
108
|
+
if (options.verbose) {
|
|
109
|
+
console.log(`🚫 ${relativePath} - Ignored (.aiwardenignore)`);
|
|
90
110
|
}
|
|
91
|
-
|
|
111
|
+
continue;
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
const content = fs.readFileSync(file, 'utf-8');
|
|
@@ -106,29 +126,62 @@ function scanCommand(targetPath, options = {}) {
|
|
|
106
126
|
scannedCount++;
|
|
107
127
|
|
|
108
128
|
if (!result.passed) {
|
|
129
|
+
// Get ignored patterns for this file
|
|
130
|
+
const ignoredPatterns = ignoreParser.getIgnoredPatterns(file);
|
|
131
|
+
|
|
132
|
+
// Filter out ignored patterns
|
|
133
|
+
const activeFindings = result.findings.filter(f =>
|
|
134
|
+
!ignoredPatterns.includes(f.id)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (activeFindings.length === 0) {
|
|
138
|
+
// All findings are ignored
|
|
139
|
+
if (options.verbose) {
|
|
140
|
+
console.log(`🚫 ${relativePath} - Patterns ignored`);
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
109
145
|
totalThreats++;
|
|
110
|
-
console.log(`⚠️ ${
|
|
146
|
+
console.log(`⚠️ ${relativePath}`);
|
|
111
147
|
console.log(` Risk: ${result.riskLevel} (Score: ${result.riskScore})`);
|
|
112
148
|
|
|
113
|
-
if (options.verbose &&
|
|
114
|
-
|
|
149
|
+
if (options.verbose && activeFindings.length > 0) {
|
|
150
|
+
activeFindings.slice(0, 3).forEach(f => {
|
|
115
151
|
console.log(` - ${f.severity}: ${f.description}`);
|
|
116
152
|
});
|
|
117
153
|
}
|
|
118
|
-
|
|
154
|
+
|
|
155
|
+
// Interactive mode: prompt user
|
|
156
|
+
if (interactivePrompt && activeFindings.length > 0) {
|
|
157
|
+
const action = await interactivePrompt.promptThreat(file, activeFindings[0]);
|
|
158
|
+
|
|
159
|
+
if (action === 'quit') {
|
|
160
|
+
console.log('\n🛑 Scan aborted by user\n');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
} else if (action === 'ignore-file' || action === 'ignore-pattern') {
|
|
163
|
+
totalThreats--; // Don't count this as a threat anymore
|
|
164
|
+
ignoredCount++;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
console.log('');
|
|
168
|
+
}
|
|
119
169
|
} else if (options.verbose) {
|
|
120
|
-
console.log(`✅ ${
|
|
170
|
+
console.log(`✅ ${relativePath} - Clean`);
|
|
121
171
|
}
|
|
122
172
|
} catch (err) {
|
|
123
173
|
if (options.verbose) {
|
|
124
174
|
console.log(`⚠️ ${path.relative(process.cwd(), file)} - Skipped (${err.message})`);
|
|
125
175
|
}
|
|
126
176
|
}
|
|
127
|
-
}
|
|
177
|
+
}
|
|
128
178
|
|
|
129
179
|
console.log(`\n${'='.repeat(60)}`);
|
|
130
180
|
console.log(`📊 Scan complete:`);
|
|
131
181
|
console.log(` Files scanned: ${scannedCount}`);
|
|
182
|
+
if (ignoredCount > 0) {
|
|
183
|
+
console.log(` Files ignored: ${ignoredCount}`);
|
|
184
|
+
}
|
|
132
185
|
console.log(` Threats found: ${totalThreats}`);
|
|
133
186
|
console.log(`${'='.repeat(60)}\n`);
|
|
134
187
|
|
|
@@ -183,6 +236,8 @@ function parseArgs() {
|
|
|
183
236
|
i++;
|
|
184
237
|
} else if (args[i] === '--verbose') {
|
|
185
238
|
options.verbose = true;
|
|
239
|
+
} else if (args[i] === '--interactive' || args[i] === '-i') {
|
|
240
|
+
options.interactive = true;
|
|
186
241
|
}
|
|
187
242
|
}
|
|
188
243
|
|
package/src/fileClassifier.js
CHANGED
|
@@ -102,7 +102,7 @@ function getAdjustedThreshold(fileType, baseThreshold) {
|
|
|
102
102
|
const multipliers = {
|
|
103
103
|
'skip': 0, // Will be skipped anyway
|
|
104
104
|
'documentation': 4.0, // Very permissive (600 for balanced mode)
|
|
105
|
-
'test':
|
|
105
|
+
'test': 3.5, // Very permissive (525 for balanced) - test data often triggers patterns
|
|
106
106
|
'api-spec': 5.0, // Extremely permissive (750 for balanced)
|
|
107
107
|
'config': 2.0, // Permissive (300 for balanced)
|
|
108
108
|
'code': 1.0 // Normal threshold
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .aiwardenignore Parser
|
|
3
|
+
* Handles whitelist/ignore rules for files and patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class IgnoreParser {
|
|
10
|
+
constructor(ignoreFilePath = '.aiwardenignore') {
|
|
11
|
+
this.ignoreFilePath = ignoreFilePath;
|
|
12
|
+
this.rules = [];
|
|
13
|
+
this.load();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load and parse .aiwardenignore file
|
|
18
|
+
*/
|
|
19
|
+
load() {
|
|
20
|
+
if (!fs.existsSync(this.ignoreFilePath)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const content = fs.readFileSync(this.ignoreFilePath, 'utf-8');
|
|
25
|
+
const lines = content.split('\n');
|
|
26
|
+
|
|
27
|
+
lines.forEach(line => {
|
|
28
|
+
// Skip comments and empty lines
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if it's a file:pattern rule
|
|
35
|
+
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
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
// It's a file/directory pattern
|
|
44
|
+
this.rules.push({
|
|
45
|
+
type: 'file',
|
|
46
|
+
pattern: trimmed
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a file should be ignored completely
|
|
54
|
+
*/
|
|
55
|
+
shouldIgnoreFile(filePath) {
|
|
56
|
+
const normalized = path.normalize(filePath);
|
|
57
|
+
|
|
58
|
+
for (const rule of this.rules) {
|
|
59
|
+
if (rule.type === 'file-pattern' && rule.patterns === '*') {
|
|
60
|
+
// Check if file matches
|
|
61
|
+
if (this.matchPath(normalized, rule.filePath)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
} else if (rule.type === 'file') {
|
|
65
|
+
if (this.matchPath(normalized, rule.pattern)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get patterns to ignore for a specific file
|
|
76
|
+
*/
|
|
77
|
+
getIgnoredPatterns(filePath) {
|
|
78
|
+
const normalized = path.normalize(filePath);
|
|
79
|
+
const ignored = [];
|
|
80
|
+
|
|
81
|
+
for (const rule of this.rules) {
|
|
82
|
+
if (rule.type === 'file-pattern' && rule.patterns !== '*') {
|
|
83
|
+
if (this.matchPath(normalized, rule.filePath)) {
|
|
84
|
+
ignored.push(...rule.patterns);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return ignored;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Match a file path against a pattern (supports wildcards)
|
|
94
|
+
*/
|
|
95
|
+
matchPath(filePath, pattern) {
|
|
96
|
+
// Exact match
|
|
97
|
+
if (filePath === pattern || filePath.endsWith(pattern)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Directory match (ends with /)
|
|
102
|
+
if (pattern.endsWith('/')) {
|
|
103
|
+
const dir = pattern.slice(0, -1);
|
|
104
|
+
if (filePath.includes(dir + path.sep) || filePath.startsWith(dir + path.sep)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Wildcard patterns
|
|
110
|
+
if (pattern.includes('*')) {
|
|
111
|
+
const regex = this.patternToRegex(pattern);
|
|
112
|
+
return regex.test(filePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert glob pattern to regex
|
|
120
|
+
*/
|
|
121
|
+
patternToRegex(pattern) {
|
|
122
|
+
// Escape special regex characters except *
|
|
123
|
+
let regex = pattern
|
|
124
|
+
.replace(/\./g, '\\.')
|
|
125
|
+
.replace(/\//g, '[\\\\/]') // Match both / and \
|
|
126
|
+
.replace(/\*\*/g, '<<<DOUBLESTAR>>>')
|
|
127
|
+
.replace(/\*/g, '[^\\\\/]*')
|
|
128
|
+
.replace(/<<<DOUBLESTAR>>>/g, '.*');
|
|
129
|
+
|
|
130
|
+
return new RegExp('^' + regex + '$');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Add a new ignore rule and save to file
|
|
135
|
+
*/
|
|
136
|
+
addIgnoreRule(filePath, patterns = '*') {
|
|
137
|
+
const rule = patterns === '*'
|
|
138
|
+
? filePath
|
|
139
|
+
: `${filePath}:${Array.isArray(patterns) ? patterns.join(',') : patterns}`;
|
|
140
|
+
|
|
141
|
+
// Check if rule already exists
|
|
142
|
+
const content = fs.existsSync(this.ignoreFilePath)
|
|
143
|
+
? fs.readFileSync(this.ignoreFilePath, 'utf-8')
|
|
144
|
+
: '';
|
|
145
|
+
|
|
146
|
+
if (content.includes(rule)) {
|
|
147
|
+
return; // Already exists
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Append rule
|
|
151
|
+
const newContent = content.trim()
|
|
152
|
+
? content.trim() + '\n' + rule + '\n'
|
|
153
|
+
: rule + '\n';
|
|
154
|
+
|
|
155
|
+
fs.writeFileSync(this.ignoreFilePath, newContent, 'utf-8');
|
|
156
|
+
|
|
157
|
+
// Reload rules
|
|
158
|
+
this.rules = [];
|
|
159
|
+
this.load();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = IgnoreParser;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Prompt for Whitelist Management
|
|
3
|
+
* Allows users to whitelist files/patterns when threats are detected
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
class InteractivePrompt {
|
|
9
|
+
constructor(ignoreParser) {
|
|
10
|
+
this.ignoreParser = ignoreParser;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prompt user what to do with a detected threat
|
|
15
|
+
* Returns: 'ignore-file' | 'ignore-pattern' | 'keep' | 'quit'
|
|
16
|
+
*/
|
|
17
|
+
async promptThreat(filePath, finding) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log('\x1b[33m⚠️ Threat detected:\x1b[0m');
|
|
26
|
+
console.log(` File: \x1b[36m${filePath}\x1b[0m`);
|
|
27
|
+
console.log(` Pattern: \x1b[31m${finding.id}\x1b[0m - ${finding.name}`);
|
|
28
|
+
console.log(` Risk: ${finding.severity} (Score: ${finding.score})`);
|
|
29
|
+
|
|
30
|
+
if (finding.match) {
|
|
31
|
+
const preview = finding.match.length > 60
|
|
32
|
+
? finding.match.substring(0, 60) + '...'
|
|
33
|
+
: finding.match;
|
|
34
|
+
console.log(` Found: "${preview}"`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(' \x1b[32m[I]\x1b[0m Ignore this entire file');
|
|
39
|
+
console.log(` \x1b[32m[P]\x1b[0m Ignore pattern ${finding.id} only`);
|
|
40
|
+
console.log(' \x1b[32m[K]\x1b[0m Keep (this is a real threat)');
|
|
41
|
+
console.log(' \x1b[32m[Q]\x1b[0m Quit scanning');
|
|
42
|
+
console.log('');
|
|
43
|
+
|
|
44
|
+
rl.question(' Choice: ', (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
|
|
47
|
+
const choice = answer.toLowerCase().trim();
|
|
48
|
+
|
|
49
|
+
if (choice === 'i') {
|
|
50
|
+
this.ignoreParser.addIgnoreRule(filePath, '*');
|
|
51
|
+
console.log(` ✅ Added to .aiwardenignore: ${filePath}`);
|
|
52
|
+
resolve('ignore-file');
|
|
53
|
+
} else if (choice === 'p') {
|
|
54
|
+
this.ignoreParser.addIgnoreRule(filePath, finding.id);
|
|
55
|
+
console.log(` ✅ Added to .aiwardenignore: ${filePath}:${finding.id}`);
|
|
56
|
+
resolve('ignore-pattern');
|
|
57
|
+
} else if (choice === 'k') {
|
|
58
|
+
console.log(' ⚠️ Kept as threat');
|
|
59
|
+
resolve('keep');
|
|
60
|
+
} else if (choice === 'q') {
|
|
61
|
+
console.log(' 🛑 Scan aborted');
|
|
62
|
+
resolve('quit');
|
|
63
|
+
} else {
|
|
64
|
+
console.log(' Invalid choice, treating as "keep"');
|
|
65
|
+
resolve('keep');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Show summary at the end
|
|
73
|
+
*/
|
|
74
|
+
showSummary(addedRules) {
|
|
75
|
+
if (addedRules.length === 0) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log('');
|
|
80
|
+
console.log('\x1b[32m✅ Whitelist updated:\x1b[0m');
|
|
81
|
+
addedRules.forEach(rule => {
|
|
82
|
+
console.log(` - ${rule}`);
|
|
83
|
+
});
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(' Run scan again to skip these files/patterns.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = InteractivePrompt;
|