@neurcode-ai/cli 0.7.12 → 0.8.1
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/dist/api-client.d.ts +38 -18
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +226 -121
- package/dist/api-client.js.map +1 -1
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +127 -1
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +56 -13
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +14 -11
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +95 -33
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +47 -57
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +245 -11
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/revert.d.ts.map +1 -1
- package/dist/commands/revert.js +82 -0
- package/dist/commands/revert.js.map +1 -1
- package/dist/commands/session.d.ts +29 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +382 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +177 -8
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +20 -15
- package/dist/commands/watch.js.map +1 -1
- package/dist/index.js +78 -3
- package/dist/index.js.map +1 -1
- package/dist/services/integrations/TicketService.d.ts +68 -0
- package/dist/services/integrations/TicketService.d.ts.map +1 -0
- package/dist/services/integrations/TicketService.js +151 -0
- package/dist/services/integrations/TicketService.js.map +1 -0
- package/dist/services/security/SecurityGuard.d.ts +88 -0
- package/dist/services/security/SecurityGuard.d.ts.map +1 -0
- package/dist/services/security/SecurityGuard.js +576 -0
- package/dist/services/security/SecurityGuard.js.map +1 -0
- package/dist/services/watch/Syncer.d.ts.map +1 -1
- package/dist/services/watch/Syncer.js +22 -1
- package/dist/services/watch/Syncer.js.map +1 -1
- package/dist/utils/ROILogger.d.ts +16 -0
- package/dist/utils/ROILogger.d.ts.map +1 -0
- package/dist/utils/ROILogger.js +45 -0
- package/dist/utils/ROILogger.js.map +1 -0
- package/dist/utils/box.d.ts +16 -0
- package/dist/utils/box.d.ts.map +1 -0
- package/dist/utils/box.js +85 -0
- package/dist/utils/box.js.map +1 -0
- package/dist/utils/messages.d.ts +81 -0
- package/dist/utils/messages.d.ts.map +1 -0
- package/dist/utils/messages.js +306 -0
- package/dist/utils/messages.js.map +1 -0
- package/dist/utils/tier.d.ts +21 -0
- package/dist/utils/tier.d.ts.map +1 -0
- package/dist/utils/tier.js +150 -0
- package/dist/utils/tier.js.map +1 -0
- package/dist/utils/user-context.d.ts +28 -0
- package/dist/utils/user-context.d.ts.map +1 -0
- package/dist/utils/user-context.js +68 -0
- package/dist/utils/user-context.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Guard - Shadow AI Shield
|
|
3
|
+
*
|
|
4
|
+
* Local privacy scanner that runs before any API call to detect and mask secrets.
|
|
5
|
+
* Uses regex patterns and AST analysis (via ts-morph) to identify sensitive data.
|
|
6
|
+
* Also includes hallucination detection for phantom packages.
|
|
7
|
+
*/
|
|
8
|
+
export interface SecretDetection {
|
|
9
|
+
type: 'aws_key' | 'bearer_token' | 'github_token' | 'generic_secret' | 'ast_literal';
|
|
10
|
+
severity: 'high' | 'medium' | 'low';
|
|
11
|
+
location: string;
|
|
12
|
+
pattern: string;
|
|
13
|
+
masked?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface HallucinationDetection {
|
|
16
|
+
packageName: string;
|
|
17
|
+
location: string;
|
|
18
|
+
importStatement: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ScanResult {
|
|
21
|
+
secrets: SecretDetection[];
|
|
22
|
+
hasSecrets: boolean;
|
|
23
|
+
maskedText?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface HallucinationScanResult {
|
|
26
|
+
hallucinations: HallucinationDetection[];
|
|
27
|
+
hasHallucinations: boolean;
|
|
28
|
+
blocked: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Security Guard for local secret detection and hallucination detection
|
|
32
|
+
*/
|
|
33
|
+
export declare class SecurityGuard {
|
|
34
|
+
private readonly REDACTION_PLACEHOLDER;
|
|
35
|
+
private readonly patterns;
|
|
36
|
+
private readonly sensitiveVarNames;
|
|
37
|
+
private readonly safePackageList;
|
|
38
|
+
/**
|
|
39
|
+
* Scan text content for secrets using regex patterns
|
|
40
|
+
*/
|
|
41
|
+
scanText(text: string, location?: string): SecretDetection[];
|
|
42
|
+
/**
|
|
43
|
+
* Extract package names from import/require statements (GREEDY - catches all patterns)
|
|
44
|
+
* Returns array of { packageName, importStatement }
|
|
45
|
+
*/
|
|
46
|
+
private extractPackageImports;
|
|
47
|
+
/**
|
|
48
|
+
* Load package.json dependencies from project root
|
|
49
|
+
*/
|
|
50
|
+
private loadProjectDependencies;
|
|
51
|
+
/**
|
|
52
|
+
* Extract package names mentioned in text (heuristic for plan summaries/reasons)
|
|
53
|
+
* Looks for patterns like: 'package-name', "package-name", library 'package-name', etc.
|
|
54
|
+
*/
|
|
55
|
+
private extractPackageMentions;
|
|
56
|
+
/**
|
|
57
|
+
* Scan code for hallucinated packages (phantom packages)
|
|
58
|
+
* Checks against safe list and project's package.json
|
|
59
|
+
* Now includes heuristic detection for package names mentioned in text (not just import statements)
|
|
60
|
+
*
|
|
61
|
+
* PRO feature only - FREE users get a message to upgrade
|
|
62
|
+
*/
|
|
63
|
+
scanForHallucinations(code: string, location?: string, rootDir?: string): Promise<HallucinationScanResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Scan TypeScript/JavaScript files using AST analysis
|
|
66
|
+
*/
|
|
67
|
+
scanFile(filePath: string, rootDir?: string): Promise<SecretDetection[]>;
|
|
68
|
+
/**
|
|
69
|
+
* Scan multiple files
|
|
70
|
+
*/
|
|
71
|
+
scanFiles(filePaths: string[], rootDir?: string): Promise<ScanResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Scan intent string for secrets
|
|
74
|
+
*/
|
|
75
|
+
scanIntent(intent: string): ScanResult;
|
|
76
|
+
/**
|
|
77
|
+
* Mask secrets in text
|
|
78
|
+
*/
|
|
79
|
+
maskSecrets(text: string, detections: SecretDetection[]): string;
|
|
80
|
+
/**
|
|
81
|
+
* Complete scan with masking
|
|
82
|
+
*/
|
|
83
|
+
scanAndMask(intent: string, filePaths: string[], rootDir?: string): Promise<ScanResult & {
|
|
84
|
+
maskedIntent?: string;
|
|
85
|
+
maskedFiles?: Map<string, string>;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=SecurityGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SecurityGuard.d.ts","sourceRoot":"","sources":["../../../src/services/security/SecurityGuard.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,cAAc,GAAG,cAAc,GAAG,gBAAgB,GAAG,aAAa,CAAC;IACrF,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,sBAAsB,EAAE,CAAC;IACzC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA4B;IAGlE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAYvB;IAGF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAQhC;IAIF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAwB7B;IAEH;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAe,GAAG,eAAe,EAAE;IAyDpE;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAoF7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAiC/B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAuF9B;;;;;;OAMG;IACG,qBAAqB,CACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,MAAe,EACzB,OAAO,GAAE,MAAsB,GAC9B,OAAO,CAAC,uBAAuB,CAAC;IAwFnC;;OAEG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,MAAsB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAyE7F;;OAEG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,MAAsB,GAAG,OAAO,CAAC,UAAU,CAAC;IAc1F;;OAEG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU;IAQtC;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,eAAe,EAAE,GAAG,MAAM;IAgBhE;;OAEG;IACG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,MAAsB,GAAG,OAAO,CAAC,UAAU,GAAG;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CAkD5K"}
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security Guard - Shadow AI Shield
|
|
4
|
+
*
|
|
5
|
+
* Local privacy scanner that runs before any API call to detect and mask secrets.
|
|
6
|
+
* Uses regex patterns and AST analysis (via ts-morph) to identify sensitive data.
|
|
7
|
+
* Also includes hallucination detection for phantom packages.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.SecurityGuard = void 0;
|
|
44
|
+
const ts_morph_1 = require("ts-morph");
|
|
45
|
+
const fs_1 = require("fs");
|
|
46
|
+
const path_1 = require("path");
|
|
47
|
+
/**
|
|
48
|
+
* Security Guard for local secret detection and hallucination detection
|
|
49
|
+
*/
|
|
50
|
+
class SecurityGuard {
|
|
51
|
+
REDACTION_PLACEHOLDER = '[REDACTED_BY_NEURCODE]';
|
|
52
|
+
// High-performance regex patterns for common secrets
|
|
53
|
+
patterns = {
|
|
54
|
+
// AWS Access Keys (AKIA followed by 16 base32 characters)
|
|
55
|
+
aws_key: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
56
|
+
// Bearer tokens / API keys (common patterns)
|
|
57
|
+
bearer_token: /\b(bearer|token|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9_\-]{32,})['"]?/gi,
|
|
58
|
+
// GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
59
|
+
github_token: /\b(ghp|gho|ghu|ghs|ghr)_[a-zA-Z0-9]{36,}\b/g,
|
|
60
|
+
// Generic high-entropy strings labeled as secrets
|
|
61
|
+
generic_secret: /\b(password|secret|key|token|api[_-]?key|private[_-]?key)\s*[:=]\s*['"]?([a-zA-Z0-9_\-+/=]{20,})['"]?/gi,
|
|
62
|
+
};
|
|
63
|
+
// Variable names that suggest secrets (case-insensitive)
|
|
64
|
+
sensitiveVarNames = [
|
|
65
|
+
/api[_-]?key/i,
|
|
66
|
+
/secret/i,
|
|
67
|
+
/password/i,
|
|
68
|
+
/token/i,
|
|
69
|
+
/private[_-]?key/i,
|
|
70
|
+
/access[_-]?token/i,
|
|
71
|
+
/auth[_-]?token/i,
|
|
72
|
+
];
|
|
73
|
+
// Safe list of common packages and standard library modules
|
|
74
|
+
// Using Set for O(1) lookup performance
|
|
75
|
+
safePackageList = new Set([
|
|
76
|
+
// Standard Node.js modules
|
|
77
|
+
'fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'events', 'stream',
|
|
78
|
+
'buffer', 'process', 'child_process', 'cluster', 'dgram', 'dns', 'net', 'readline',
|
|
79
|
+
'repl', 'tls', 'tty', 'vm', 'zlib', 'assert', 'querystring', 'string_decoder',
|
|
80
|
+
'timers', 'punycode', 'v8', 'worker_threads', 'perf_hooks', 'async_hooks',
|
|
81
|
+
'inspector', 'module', 'console', 'domain', 'constants',
|
|
82
|
+
// Common npm packages (top 100 most popular)
|
|
83
|
+
'react', 'react-dom', 'lodash', 'express', 'axios', 'moment', 'vue', 'angular',
|
|
84
|
+
'typescript', 'webpack', 'babel', 'jest', 'mocha', 'chai', 'sinon', 'eslint',
|
|
85
|
+
'prettier', 'next', 'gatsby', 'nuxt', 'svelte', 'rxjs', 'redux', 'mobx',
|
|
86
|
+
'styled-components', 'emotion', 'tailwindcss', 'bootstrap', 'material-ui',
|
|
87
|
+
'@mui/material', '@mui/icons-material', 'antd', 'semantic-ui', 'chakra-ui',
|
|
88
|
+
'node-fetch', 'got', 'request', 'superagent', 'cheerio', 'puppeteer', 'playwright',
|
|
89
|
+
'mongoose', 'sequelize', 'typeorm', 'prisma', 'knex', 'pg', 'mysql2', 'sqlite3',
|
|
90
|
+
'redis', 'ioredis', 'ws', 'socket.io', 'graphql', 'apollo', 'relay',
|
|
91
|
+
'dotenv', 'cross-env', 'nodemon', 'pm2', 'forever', 'concurrently',
|
|
92
|
+
'uuid', 'nanoid', 'crypto-js', 'bcrypt', 'jsonwebtoken', 'passport',
|
|
93
|
+
'winston', 'morgan', 'pino', 'debug', 'chalk', 'colors', 'commander',
|
|
94
|
+
'yargs', 'inquirer', 'ora', 'listr', 'glob', 'minimist', 'dot-prop',
|
|
95
|
+
'fast-glob', 'micromatch', 'rimraf', 'mkdirp', 'fs-extra', 'graceful-fs',
|
|
96
|
+
'chokidar', 'watchman', 'nodemailer', 'handlebars', 'ejs', 'pug', 'mustache',
|
|
97
|
+
'marked', 'highlight.js', 'prismjs', 'showdown', 'remark', 'rehype',
|
|
98
|
+
]);
|
|
99
|
+
/**
|
|
100
|
+
* Scan text content for secrets using regex patterns
|
|
101
|
+
*/
|
|
102
|
+
scanText(text, location = 'text') {
|
|
103
|
+
const detections = [];
|
|
104
|
+
// Check AWS keys
|
|
105
|
+
const awsMatches = text.match(this.patterns.aws_key);
|
|
106
|
+
if (awsMatches) {
|
|
107
|
+
awsMatches.forEach(match => {
|
|
108
|
+
detections.push({
|
|
109
|
+
type: 'aws_key',
|
|
110
|
+
severity: 'high',
|
|
111
|
+
location,
|
|
112
|
+
pattern: match,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Check GitHub tokens
|
|
117
|
+
const githubMatches = text.match(this.patterns.github_token);
|
|
118
|
+
if (githubMatches) {
|
|
119
|
+
githubMatches.forEach(match => {
|
|
120
|
+
detections.push({
|
|
121
|
+
type: 'github_token',
|
|
122
|
+
severity: 'high',
|
|
123
|
+
location,
|
|
124
|
+
pattern: match,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Check bearer tokens and generic secrets
|
|
129
|
+
const bearerMatches = Array.from(text.matchAll(this.patterns.bearer_token));
|
|
130
|
+
bearerMatches.forEach(match => {
|
|
131
|
+
if (match[2] && match[2].length >= 32) {
|
|
132
|
+
detections.push({
|
|
133
|
+
type: 'bearer_token',
|
|
134
|
+
severity: 'high',
|
|
135
|
+
location,
|
|
136
|
+
pattern: match[0],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const genericMatches = Array.from(text.matchAll(this.patterns.generic_secret));
|
|
141
|
+
genericMatches.forEach(match => {
|
|
142
|
+
if (match[2] && match[2].length >= 20) {
|
|
143
|
+
detections.push({
|
|
144
|
+
type: 'generic_secret',
|
|
145
|
+
severity: match[1]?.toLowerCase().includes('password') ? 'high' : 'medium',
|
|
146
|
+
location,
|
|
147
|
+
pattern: match[0],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return detections;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Extract package names from import/require statements (GREEDY - catches all patterns)
|
|
155
|
+
* Returns array of { packageName, importStatement }
|
|
156
|
+
*/
|
|
157
|
+
extractPackageImports(code) {
|
|
158
|
+
const imports = [];
|
|
159
|
+
const seenStatements = new Set(); // Track to avoid duplicates
|
|
160
|
+
// Pattern 1: ES6 import statements (GREEDY - matches all variations)
|
|
161
|
+
// import x from 'package'
|
|
162
|
+
// import { x, y } from 'package'
|
|
163
|
+
// import * as x from 'package'
|
|
164
|
+
// import x, { y } from 'package'
|
|
165
|
+
// import type { x } from 'package'
|
|
166
|
+
// import 'package' (side-effect imports)
|
|
167
|
+
const es6ImportPattern = /import\s+(?:(?:type\s+)?(?:\*\s+as\s+\w+)|(?:\{[^}]*\})|(?:\w+)|(?:\w+\s*,\s*\{[^}]*\})|(?:type\s+\{[^}]*\}))\s+from\s+['"]([^'"]+)['"]/g;
|
|
168
|
+
// Pattern 2: Side-effect imports (standalone import 'package')
|
|
169
|
+
const sideEffectImportPattern = /^import\s+['"]([^'"]+)['"];?$/gm;
|
|
170
|
+
// Pattern 3: CommonJS require (GREEDY - matches all variations)
|
|
171
|
+
// const x = require('package')
|
|
172
|
+
// const { x } = require('package')
|
|
173
|
+
// require('package')
|
|
174
|
+
// module.exports = require('package')
|
|
175
|
+
const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
176
|
+
// Pattern 4: Dynamic/lazy imports (GREEDY)
|
|
177
|
+
// import('package')
|
|
178
|
+
// await import('package')
|
|
179
|
+
// const x = import('package')
|
|
180
|
+
const dynamicImportPattern = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
181
|
+
// Extract ES6 imports with 'from' clause
|
|
182
|
+
let match;
|
|
183
|
+
while ((match = es6ImportPattern.exec(code)) !== null) {
|
|
184
|
+
const packageName = match[1];
|
|
185
|
+
if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
186
|
+
const importStatement = match[0].trim();
|
|
187
|
+
if (!seenStatements.has(importStatement)) {
|
|
188
|
+
imports.push({ packageName, importStatement });
|
|
189
|
+
seenStatements.add(importStatement);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Extract side-effect imports (reset regex lastIndex)
|
|
194
|
+
sideEffectImportPattern.lastIndex = 0;
|
|
195
|
+
while ((match = sideEffectImportPattern.exec(code)) !== null) {
|
|
196
|
+
const packageName = match[1];
|
|
197
|
+
if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
198
|
+
const importStatement = match[0].trim();
|
|
199
|
+
if (!seenStatements.has(importStatement)) {
|
|
200
|
+
imports.push({ packageName, importStatement });
|
|
201
|
+
seenStatements.add(importStatement);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Extract require statements
|
|
206
|
+
requirePattern.lastIndex = 0;
|
|
207
|
+
while ((match = requirePattern.exec(code)) !== null) {
|
|
208
|
+
const packageName = match[1];
|
|
209
|
+
if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
210
|
+
const importStatement = match[0].trim();
|
|
211
|
+
if (!seenStatements.has(importStatement)) {
|
|
212
|
+
imports.push({ packageName, importStatement });
|
|
213
|
+
seenStatements.add(importStatement);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Extract dynamic/lazy imports
|
|
218
|
+
dynamicImportPattern.lastIndex = 0;
|
|
219
|
+
while ((match = dynamicImportPattern.exec(code)) !== null) {
|
|
220
|
+
const packageName = match[1];
|
|
221
|
+
if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
222
|
+
const importStatement = match[0].trim();
|
|
223
|
+
if (!seenStatements.has(importStatement)) {
|
|
224
|
+
imports.push({ packageName, importStatement });
|
|
225
|
+
seenStatements.add(importStatement);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return imports;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Load package.json dependencies from project root
|
|
233
|
+
*/
|
|
234
|
+
loadProjectDependencies(rootDir = process.cwd()) {
|
|
235
|
+
const packageJsonPath = (0, path_1.join)(rootDir, 'package.json');
|
|
236
|
+
const dependencies = new Set();
|
|
237
|
+
if (!(0, fs_1.existsSync)(packageJsonPath)) {
|
|
238
|
+
return dependencies;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const packageJson = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, 'utf-8'));
|
|
242
|
+
// Add dependencies
|
|
243
|
+
if (packageJson.dependencies && typeof packageJson.dependencies === 'object') {
|
|
244
|
+
Object.keys(packageJson.dependencies).forEach(pkg => dependencies.add(pkg));
|
|
245
|
+
}
|
|
246
|
+
// Add devDependencies
|
|
247
|
+
if (packageJson.devDependencies && typeof packageJson.devDependencies === 'object') {
|
|
248
|
+
Object.keys(packageJson.devDependencies).forEach(pkg => dependencies.add(pkg));
|
|
249
|
+
}
|
|
250
|
+
// Add peerDependencies
|
|
251
|
+
if (packageJson.peerDependencies && typeof packageJson.peerDependencies === 'object') {
|
|
252
|
+
Object.keys(packageJson.peerDependencies).forEach(pkg => dependencies.add(pkg));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
// If package.json can't be read or parsed, return empty set
|
|
257
|
+
// This is non-fatal - we'll just rely on safe list
|
|
258
|
+
}
|
|
259
|
+
return dependencies;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Extract package names mentioned in text (heuristic for plan summaries/reasons)
|
|
263
|
+
* Looks for patterns like: 'package-name', "package-name", library 'package-name', etc.
|
|
264
|
+
*/
|
|
265
|
+
extractPackageMentions(text) {
|
|
266
|
+
const mentions = [];
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
// Pattern 1: Package names in quotes with keyword BEFORE (e.g., "uses 'package'", "library 'package'")
|
|
269
|
+
// Matches: uses 'react-ultimate-super-charts', library "package", package 'package'
|
|
270
|
+
const quotedPackagePatternBefore = /(?:library|package|uses?|imports?|requires?|from|using|depends?|dependency)\s+['"]([a-zA-Z0-9@][a-zA-Z0-9._-]*[a-zA-Z0-9])['"]/gi;
|
|
271
|
+
// Pattern 2: Package names in quotes with keyword AFTER (e.g., "'package' library", "'package' package")
|
|
272
|
+
// Matches: 'react-ultimate-super-charts' library, "package-name" package
|
|
273
|
+
const quotedPackagePatternAfter = /['"]([a-zA-Z0-9@][a-zA-Z0-9._-]*[a-zA-Z0-9])['"]\s+(?:library|package|npm|module)/gi;
|
|
274
|
+
// Pattern 3: Standalone quoted package names (more aggressive - checks context)
|
|
275
|
+
// Matches: 'package-name' or "package-name" when in package-related context
|
|
276
|
+
const standaloneQuotedPattern = /['"]([a-zA-Z0-9@][a-zA-Z0-9._-]*[a-zA-Z0-9])['"]/g;
|
|
277
|
+
// Extract from library/package mentions (keyword BEFORE)
|
|
278
|
+
let match;
|
|
279
|
+
while ((match = quotedPackagePatternBefore.exec(text)) !== null) {
|
|
280
|
+
const packageName = match[1];
|
|
281
|
+
// Validate it looks like a package name (not a file path, not too short)
|
|
282
|
+
if (packageName && packageName.length >= 2 && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
283
|
+
if (!seen.has(packageName)) {
|
|
284
|
+
mentions.push({ packageName, context: match[0] });
|
|
285
|
+
seen.add(packageName);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Extract from library/package mentions (keyword AFTER)
|
|
290
|
+
quotedPackagePatternAfter.lastIndex = 0;
|
|
291
|
+
while ((match = quotedPackagePatternAfter.exec(text)) !== null) {
|
|
292
|
+
const packageName = match[1];
|
|
293
|
+
if (packageName && packageName.length >= 2 && !packageName.startsWith('.') && !packageName.startsWith('/')) {
|
|
294
|
+
if (!seen.has(packageName)) {
|
|
295
|
+
mentions.push({ packageName, context: match[0] });
|
|
296
|
+
seen.add(packageName);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Extract standalone quoted strings (but exclude if already found)
|
|
301
|
+
// This is a fallback for cases like "uses the 'package-name' library" or other variations
|
|
302
|
+
standaloneQuotedPattern.lastIndex = 0;
|
|
303
|
+
while ((match = standaloneQuotedPattern.exec(text)) !== null) {
|
|
304
|
+
const packageName = match[1];
|
|
305
|
+
// Only consider if it looks like an npm package name
|
|
306
|
+
// Valid package names: start with letter/number/@, contain letters/numbers/dots/dashes/underscores
|
|
307
|
+
const isValidPackageName = /^[a-zA-Z0-9@][a-zA-Z0-9._-]*[a-zA-Z0-9]$/.test(packageName);
|
|
308
|
+
// Exclude component names (PascalCase single words that are likely React components)
|
|
309
|
+
// Component names are typically single capitalized words like "DataVisualizer", "Button", etc.
|
|
310
|
+
const isLikelyComponentName = /^[A-Z][a-zA-Z0-9]*$/.test(packageName) &&
|
|
311
|
+
!packageName.includes('-') &&
|
|
312
|
+
!packageName.includes('_') &&
|
|
313
|
+
!packageName.includes('.') &&
|
|
314
|
+
packageName.length < 30; // Components are usually shorter
|
|
315
|
+
if (isValidPackageName &&
|
|
316
|
+
packageName.length >= 2 &&
|
|
317
|
+
!packageName.startsWith('.') &&
|
|
318
|
+
!packageName.startsWith('/') &&
|
|
319
|
+
!seen.has(packageName) &&
|
|
320
|
+
!isLikelyComponentName) { // Exclude component names
|
|
321
|
+
// Check if it's mentioned in context that suggests it's a package
|
|
322
|
+
const beforeMatch = text.substring(Math.max(0, match.index - 50), match.index).toLowerCase();
|
|
323
|
+
const afterMatch = text.substring(match.index + match[0].length, Math.min(text.length, match.index + match[0].length + 50)).toLowerCase();
|
|
324
|
+
const context = beforeMatch + match[0] + afterMatch;
|
|
325
|
+
// Only flag if context suggests it's a package/library (not a file path or variable name)
|
|
326
|
+
// Require stronger evidence for standalone matches (must have library/package/npm keywords)
|
|
327
|
+
const isPackageContext = /(library|package|npm|install|import|require|from|using|uses?|depends?|dependency|module)/i.test(context);
|
|
328
|
+
const isNotFilePath = !/\.(js|ts|jsx|tsx|json|md|txt|html|css|scss|less)$/i.test(context);
|
|
329
|
+
const isNotVariable = !/(const|let|var|function|class|interface|type)\s+['"]/.test(beforeMatch);
|
|
330
|
+
const isNotStringLiteral = !/(['"])\s*\+/.test(afterMatch) && !/\+\s*(['"])/.test(beforeMatch);
|
|
331
|
+
const isNotComponentContext = /(component|class|function|interface|type)\s+['"]/.test(beforeMatch);
|
|
332
|
+
if (isPackageContext && isNotFilePath && isNotVariable && isNotStringLiteral && !isNotComponentContext) {
|
|
333
|
+
mentions.push({ packageName, context: match[0] });
|
|
334
|
+
seen.add(packageName);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return mentions;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Scan code for hallucinated packages (phantom packages)
|
|
342
|
+
* Checks against safe list and project's package.json
|
|
343
|
+
* Now includes heuristic detection for package names mentioned in text (not just import statements)
|
|
344
|
+
*
|
|
345
|
+
* PRO feature only - FREE users get a message to upgrade
|
|
346
|
+
*/
|
|
347
|
+
async scanForHallucinations(code, location = 'code', rootDir = process.cwd()) {
|
|
348
|
+
// Check user tier - Hallucination Shield is PRO only
|
|
349
|
+
const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../../utils/tier')));
|
|
350
|
+
const tier = await getUserTier();
|
|
351
|
+
if (tier === 'FREE') {
|
|
352
|
+
// Return empty result for FREE users (feature disabled)
|
|
353
|
+
// The calling code should check tier and show upgrade message
|
|
354
|
+
return {
|
|
355
|
+
hallucinations: [],
|
|
356
|
+
hasHallucinations: false,
|
|
357
|
+
blocked: false,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const hallucinations = [];
|
|
361
|
+
// Extract all package imports (from actual import/require statements)
|
|
362
|
+
const imports = this.extractPackageImports(code);
|
|
363
|
+
// Also extract package names mentioned in text (heuristic for plan summaries)
|
|
364
|
+
const mentions = this.extractPackageMentions(code);
|
|
365
|
+
// If no imports or mentions found, return early
|
|
366
|
+
if (imports.length === 0 && mentions.length === 0) {
|
|
367
|
+
return {
|
|
368
|
+
hallucinations: [],
|
|
369
|
+
hasHallucinations: false,
|
|
370
|
+
blocked: false,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Load project dependencies
|
|
374
|
+
const projectDependencies = this.loadProjectDependencies(rootDir);
|
|
375
|
+
// Check each import statement
|
|
376
|
+
for (const { packageName, importStatement } of imports) {
|
|
377
|
+
// Check if package is in safe list
|
|
378
|
+
if (this.safePackageList.has(packageName)) {
|
|
379
|
+
continue; // Safe package, skip
|
|
380
|
+
}
|
|
381
|
+
// Check if package is in project dependencies
|
|
382
|
+
if (projectDependencies.has(packageName)) {
|
|
383
|
+
continue; // Package exists in project, skip
|
|
384
|
+
}
|
|
385
|
+
// Not in safe list or dependencies - flag as hallucination
|
|
386
|
+
hallucinations.push({
|
|
387
|
+
packageName,
|
|
388
|
+
location,
|
|
389
|
+
importStatement,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// Check each package mention (from text heuristics)
|
|
393
|
+
for (const { packageName, context } of mentions) {
|
|
394
|
+
// Skip if we already flagged this package from an import statement
|
|
395
|
+
if (hallucinations.some(h => h.packageName === packageName)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
// Check if package is in safe list
|
|
399
|
+
if (this.safePackageList.has(packageName)) {
|
|
400
|
+
continue; // Safe package, skip
|
|
401
|
+
}
|
|
402
|
+
// Check if package is in project dependencies
|
|
403
|
+
if (projectDependencies.has(packageName)) {
|
|
404
|
+
continue; // Package exists in project, skip
|
|
405
|
+
}
|
|
406
|
+
// Not in safe list or dependencies - flag as hallucination
|
|
407
|
+
// Use context as the "import statement" for mentions
|
|
408
|
+
hallucinations.push({
|
|
409
|
+
packageName,
|
|
410
|
+
location,
|
|
411
|
+
importStatement: context,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
hallucinations,
|
|
416
|
+
hasHallucinations: hallucinations.length > 0,
|
|
417
|
+
blocked: hallucinations.length > 0, // Block if any hallucinations detected
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Scan TypeScript/JavaScript files using AST analysis
|
|
422
|
+
*/
|
|
423
|
+
async scanFile(filePath, rootDir = process.cwd()) {
|
|
424
|
+
const detections = [];
|
|
425
|
+
const fullPath = (0, path_1.resolve)(rootDir, filePath);
|
|
426
|
+
// Check if file exists and is a .ts or .js file
|
|
427
|
+
if (!(0, fs_1.existsSync)(fullPath)) {
|
|
428
|
+
return detections;
|
|
429
|
+
}
|
|
430
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
431
|
+
if (ext !== 'ts' && ext !== 'js' && ext !== 'tsx' && ext !== 'jsx') {
|
|
432
|
+
// For non-TS/JS files, use regex scanning
|
|
433
|
+
try {
|
|
434
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
435
|
+
return this.scanText(content, filePath);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return detections;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
// Use ts-morph for AST analysis
|
|
443
|
+
const project = new ts_morph_1.Project({
|
|
444
|
+
skipAddingFilesFromTsConfig: true,
|
|
445
|
+
skipFileDependencyResolution: true,
|
|
446
|
+
});
|
|
447
|
+
const sourceFile = project.addSourceFileAtPath(fullPath);
|
|
448
|
+
// Scan for variable declarations with sensitive names
|
|
449
|
+
sourceFile.getVariableDeclarations().forEach(variable => {
|
|
450
|
+
const name = variable.getName();
|
|
451
|
+
const initializer = variable.getInitializer();
|
|
452
|
+
// Check if variable name suggests a secret
|
|
453
|
+
const isSensitive = this.sensitiveVarNames.some(pattern => pattern.test(name));
|
|
454
|
+
if (isSensitive && initializer) {
|
|
455
|
+
// Check if initializer is a string literal
|
|
456
|
+
const kind = initializer.getKindName();
|
|
457
|
+
if (kind === 'StringLiteral' || kind === 'NoSubstitutionTemplateLiteral') {
|
|
458
|
+
const text = initializer.getText().replace(/['"`]/g, '');
|
|
459
|
+
// Check if the value looks like a secret (high entropy)
|
|
460
|
+
if (text.length >= 20 && /[a-zA-Z0-9_\-+/=]{20,}/.test(text)) {
|
|
461
|
+
detections.push({
|
|
462
|
+
type: 'ast_literal',
|
|
463
|
+
severity: 'high',
|
|
464
|
+
location: filePath,
|
|
465
|
+
pattern: `${name} = ${initializer.getText()}`,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
// Also run regex scanning as fallback
|
|
472
|
+
const content = sourceFile.getFullText();
|
|
473
|
+
const regexDetections = this.scanText(content, filePath);
|
|
474
|
+
detections.push(...regexDetections);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
// If AST parsing fails, fall back to regex scanning
|
|
478
|
+
try {
|
|
479
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
480
|
+
return this.scanText(content, filePath);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
// File cannot be read, return empty
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return detections;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Scan multiple files
|
|
490
|
+
*/
|
|
491
|
+
async scanFiles(filePaths, rootDir = process.cwd()) {
|
|
492
|
+
const allDetections = [];
|
|
493
|
+
for (const filePath of filePaths) {
|
|
494
|
+
const detections = await this.scanFile(filePath, rootDir);
|
|
495
|
+
allDetections.push(...detections);
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
secrets: allDetections,
|
|
499
|
+
hasSecrets: allDetections.length > 0,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Scan intent string for secrets
|
|
504
|
+
*/
|
|
505
|
+
scanIntent(intent) {
|
|
506
|
+
const detections = this.scanText(intent, 'intent');
|
|
507
|
+
return {
|
|
508
|
+
secrets: detections,
|
|
509
|
+
hasSecrets: detections.length > 0,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Mask secrets in text
|
|
514
|
+
*/
|
|
515
|
+
maskSecrets(text, detections) {
|
|
516
|
+
let maskedText = text;
|
|
517
|
+
// Sort detections by pattern length (longest first) to avoid partial replacements
|
|
518
|
+
const sortedDetections = [...detections].sort((a, b) => b.pattern.length - a.pattern.length);
|
|
519
|
+
for (const detection of sortedDetections) {
|
|
520
|
+
// Escape special regex characters in the pattern
|
|
521
|
+
const escapedPattern = detection.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
522
|
+
const regex = new RegExp(escapedPattern, 'g');
|
|
523
|
+
maskedText = maskedText.replace(regex, this.REDACTION_PLACEHOLDER);
|
|
524
|
+
}
|
|
525
|
+
return maskedText;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Complete scan with masking
|
|
529
|
+
*/
|
|
530
|
+
async scanAndMask(intent, filePaths, rootDir = process.cwd()) {
|
|
531
|
+
const intentResult = this.scanIntent(intent);
|
|
532
|
+
const filesResult = await this.scanFiles(filePaths, rootDir);
|
|
533
|
+
const allDetections = [...intentResult.secrets, ...filesResult.secrets];
|
|
534
|
+
const hasSecrets = allDetections.length > 0;
|
|
535
|
+
let maskedIntent;
|
|
536
|
+
let maskedFiles;
|
|
537
|
+
if (hasSecrets) {
|
|
538
|
+
// Mask intent
|
|
539
|
+
if (intentResult.hasSecrets) {
|
|
540
|
+
maskedIntent = this.maskSecrets(intent, intentResult.secrets);
|
|
541
|
+
}
|
|
542
|
+
// Mask files
|
|
543
|
+
if (filesResult.hasSecrets) {
|
|
544
|
+
maskedFiles = new Map();
|
|
545
|
+
const fileDetections = new Map();
|
|
546
|
+
// Group detections by file
|
|
547
|
+
for (const detection of filesResult.secrets) {
|
|
548
|
+
if (detection.location !== 'intent') {
|
|
549
|
+
const fileDetectionsList = fileDetections.get(detection.location) || [];
|
|
550
|
+
fileDetectionsList.push(detection);
|
|
551
|
+
fileDetections.set(detection.location, fileDetectionsList);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// Mask each file
|
|
555
|
+
for (const [filePath, detections] of fileDetections.entries()) {
|
|
556
|
+
try {
|
|
557
|
+
const content = (0, fs_1.readFileSync)((0, path_1.resolve)(rootDir, filePath), 'utf-8');
|
|
558
|
+
const masked = this.maskSecrets(content, detections);
|
|
559
|
+
maskedFiles.set(filePath, masked);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// Skip files that can't be read
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
secrets: allDetections,
|
|
569
|
+
hasSecrets,
|
|
570
|
+
maskedIntent,
|
|
571
|
+
maskedFiles,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
exports.SecurityGuard = SecurityGuard;
|
|
576
|
+
//# sourceMappingURL=SecurityGuard.js.map
|