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 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
  [![npm version](https://img.shields.io/npm/v/ai-warden.svg)](https://www.npmjs.com/package/ai-warden)
8
+ [![Tests](https://github.com/larhog/ai-warden-dev/actions/workflows/test.yml/badge.svg)](https://github.com/larhog/ai-warden-dev/actions/workflows/test.yml)
9
+ [![Security Scan](https://github.com/larhog/ai-warden-dev/actions/workflows/security-scan.yml/badge.svg)](https://github.com/larhog/ai-warden-dev/actions/workflows/security-scan.yml)
8
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-warden",
3
- "version": "0.3.0",
3
+ "version": "0.4.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
@@ -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
- files.forEach(file => {
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(`⏭️ ${path.relative(process.cwd(), file)} - Skipped`);
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
- return;
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(`⚠️ ${path.relative(process.cwd(), file)}`);
146
+ console.log(`⚠️ ${relativePath}`);
111
147
  console.log(` Risk: ${result.riskLevel} (Score: ${result.riskScore})`);
112
148
 
113
- if (options.verbose && result.findings.length > 0) {
114
- result.findings.slice(0, 3).forEach(f => {
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
- console.log('');
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(`✅ ${path.relative(process.cwd(), file)} - Clean`);
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
 
@@ -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': 2.5, // Permissive (375 for balanced)
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;