vanguard-cli 3.0.0 → 3.1.2
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/CHANGELOG.md +18 -0
- package/lib/commands/scan.js +23 -9
- package/lib/services/scanner.js +50 -50
- package/package.json +51 -50
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.1.2 (2026-01-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* implement scanner resilience and add test suite 🛡️🧪🧬✨ ([20310c3](https://github.com/bazobehram/vanguard/commit/20310c30d2787cbc2dedb0a19d8d2a5d2b308d0d))
|
|
9
|
+
* implement smart throttling and robust 429 recovery logic 🛡️❄️✨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
|
|
10
|
+
* initial public release of Vanguard V3 Enterprise Edition ��️🚀✨ ([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
|
|
11
|
+
|
|
12
|
+
## 3.1.1 (2026-01-24)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* implement smart throttling and robust 429 recovery logic 🛡️❄️✨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
|
|
18
|
+
* initial public release of Vanguard V3 Enterprise Edition ��️🚀✨ ([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
|
package/lib/commands/scan.js
CHANGED
|
@@ -37,7 +37,14 @@ export async function handlePull(cmdOptions, programOptions) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const scanner = new VanguardScanner(programOptions.model, context);
|
|
40
|
-
|
|
40
|
+
let analysis;
|
|
41
|
+
try {
|
|
42
|
+
analysis = await scanner.scan('git_diff', diff, spinner);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
spinner.fail(`AI Audit Failed: ${err.message}`);
|
|
45
|
+
console.log(chalk.yellow('\n💡 Tip: You might be rate-limited. Try again in 60s or run "vanguard config" to switch to local Ollama.'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
41
48
|
spinner.stop();
|
|
42
49
|
|
|
43
50
|
showAlert(analysis);
|
|
@@ -107,14 +114,21 @@ export async function handleClone(url, directory, programOptions) {
|
|
|
107
114
|
continue;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
try {
|
|
118
|
+
const result = await scanner.scan(file, content, scanSpinner);
|
|
119
|
+
if (result.verdict === 'BLOCK') {
|
|
120
|
+
finalVerdict = 'BLOCK';
|
|
121
|
+
allThreats.push(
|
|
122
|
+
...result.threats.map((t) => ({ ...t, file: path.relative(tempPath, file) }))
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
CacheManager.save(file, content, 'SAFE');
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
scanSpinner.fail(`Audit Interrupted: ${err.message}`);
|
|
129
|
+
console.log(chalk.yellow('\n⚠️ Service Error. Part of the codebase remains unverified.'));
|
|
130
|
+
await cleanupSandbox(tempPath);
|
|
131
|
+
return;
|
|
118
132
|
}
|
|
119
133
|
}
|
|
120
134
|
|
package/lib/services/scanner.js
CHANGED
|
@@ -10,7 +10,9 @@ import pLimit from 'p-limit';
|
|
|
10
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const THREATS_PATH = path.join(__dirname, '../threats.json');
|
|
12
12
|
|
|
13
|
-
const limit = pLimit(
|
|
13
|
+
const limit = pLimit(1); // Conservative concurrency for free tier stability
|
|
14
|
+
|
|
15
|
+
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
16
|
|
|
15
17
|
export class VanguardScanner {
|
|
16
18
|
constructor(modelOverride, intelligenceContext = '') {
|
|
@@ -19,36 +21,20 @@ export class VanguardScanner {
|
|
|
19
21
|
this.intelligenceContext = intelligenceContext;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* Enterprise Filtering Logic: Skip binaries, images, and massive files.
|
|
24
|
-
*/
|
|
25
24
|
shouldSkip(filePath, stats) {
|
|
26
25
|
const ext = path.extname(filePath).toLowerCase();
|
|
27
26
|
const binaryExtensions = [
|
|
28
|
-
'.exe',
|
|
29
|
-
'.dll',
|
|
30
|
-
'.so',
|
|
31
|
-
'.bin',
|
|
32
|
-
'.png',
|
|
33
|
-
'.jpg',
|
|
34
|
-
'.jpeg',
|
|
35
|
-
'.gif',
|
|
36
|
-
'.pdf',
|
|
37
|
-
'.zip',
|
|
38
|
-
'.gz',
|
|
27
|
+
'.exe', '.dll', '.so', '.bin', '.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip', '.gz',
|
|
39
28
|
];
|
|
40
|
-
|
|
41
29
|
if (binaryExtensions.includes(ext)) return true;
|
|
42
|
-
if (stats.size > 100 * 1024) return true;
|
|
30
|
+
if (stats.size > 100 * 1024) return true;
|
|
43
31
|
if (filePath.includes('node_modules')) return true;
|
|
44
|
-
|
|
45
32
|
return false;
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
async getSystemInstruction() {
|
|
49
36
|
const threatsData = await fs.readFile(THREATS_PATH, 'utf-8');
|
|
50
37
|
const threats = JSON.parse(threatsData);
|
|
51
|
-
|
|
52
38
|
const categoriesListing = threats
|
|
53
39
|
.map((t, i) => `${i + 1}. **${t.name}:** ${t.description}`)
|
|
54
40
|
.join('\n');
|
|
@@ -62,44 +48,59 @@ ${categoriesListing}
|
|
|
62
48
|
|
|
63
49
|
${this.intelligenceContext}
|
|
64
50
|
|
|
65
|
-
TASK: Combine the code analysis with the latest intelligence and vulnerability data above.
|
|
66
|
-
If OSV reports a critical vulnerability or the remote rules flag a new pattern, YOU MUST VERDICT: BLOCK.
|
|
67
|
-
|
|
68
51
|
JSON OUTPUT RULES:
|
|
69
|
-
- risk_score: 0-100
|
|
70
|
-
- verdict: "BLOCK"
|
|
71
|
-
- threats: Array of
|
|
72
|
-
- summary: Clear
|
|
52
|
+
- risk_score: 0-100.
|
|
53
|
+
- verdict: "BLOCK" or "SAFE".
|
|
54
|
+
- threats: Array of { "file", "line", "threat", "reason" }
|
|
55
|
+
- summary: Clear explanation.
|
|
73
56
|
|
|
74
57
|
RESPONSE FORMAT (JSON ONLY):
|
|
75
|
-
{ "risk_score": 95, "verdict": "BLOCK", "threats": [
|
|
58
|
+
{ "risk_score": 95, "verdict": "BLOCK", "threats": [], "summary": "..." }
|
|
76
59
|
`;
|
|
77
60
|
}
|
|
78
61
|
|
|
79
|
-
async scan(filePath, content) {
|
|
80
|
-
|
|
62
|
+
async scan(filePath, content, spinner = null) {
|
|
63
|
+
// Random Jitter Throttling (1s - 3s)
|
|
64
|
+
if (this.provider === 'gemini') {
|
|
65
|
+
const jitter = Math.floor(Math.random() * 2000) + 1000;
|
|
66
|
+
await sleep(jitter);
|
|
67
|
+
}
|
|
68
|
+
return limit(() => this.scanWithRetry(filePath, content, spinner));
|
|
81
69
|
}
|
|
82
70
|
|
|
83
|
-
async scanWithRetry(filePath, content,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
async scanWithRetry(filePath, content, spinner, attempt = 1) {
|
|
72
|
+
const maxAttempts = 3;
|
|
73
|
+
try {
|
|
74
|
+
if (this.provider === 'gemini') {
|
|
75
|
+
return await this.scanWithGemini(content);
|
|
76
|
+
} else {
|
|
77
|
+
return await this.scanWithOllama(content);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const isRateLimit = error.message.includes('429') || error.message.includes('Too Many Requests');
|
|
81
|
+
const isServerErr = error.message.includes('500') || error.message.includes('503');
|
|
82
|
+
|
|
83
|
+
if (isRateLimit && attempt <= maxAttempts) {
|
|
84
|
+
if (spinner) {
|
|
85
|
+
const originalText = spinner.text;
|
|
86
|
+
for (let i = 30; i > 0; i--) {
|
|
87
|
+
spinner.text = `❄️ Rate limit hit. Cooling down API (${i}s remaining)...`;
|
|
88
|
+
await sleep(1000);
|
|
89
|
+
}
|
|
90
|
+
spinner.text = originalText;
|
|
88
91
|
} else {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
} catch (error) {
|
|
92
|
-
const isRateLimit =
|
|
93
|
-
error.message.includes('429') || error.message.includes('Too Many Requests');
|
|
94
|
-
if (isRateLimit && i < retries - 1) {
|
|
95
|
-
const delay = Math.pow(2, i) * 1000;
|
|
96
|
-
if (config.get('VERBOSE'))
|
|
97
|
-
console.log(chalk.yellow(` ⚠ Rate limited. Retrying in ${delay}ms...`));
|
|
98
|
-
await new Promise((res) => setTimeout(res, delay));
|
|
99
|
-
continue;
|
|
92
|
+
console.log(chalk.yellow(`\n⚠️ Rate limit hit. Cooling down for 30s (Attempt ${attempt}/${maxAttempts})...`));
|
|
93
|
+
await sleep(30000);
|
|
100
94
|
}
|
|
101
|
-
|
|
95
|
+
return this.scanWithRetry(filePath, content, spinner, attempt + 1);
|
|
102
96
|
}
|
|
97
|
+
|
|
98
|
+
if (isServerErr && attempt === 1) {
|
|
99
|
+
if (spinner) spinner.text = '🔄 AI Server hiccup. Retrying immediately...';
|
|
100
|
+
return this.scanWithRetry(filePath, content, spinner, attempt + 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw error;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
@@ -108,13 +109,12 @@ RESPONSE FORMAT (JSON ONLY):
|
|
|
108
109
|
if (!apiKey) throw new Error('Gemini API Key missing. Run "vanguard config"');
|
|
109
110
|
|
|
110
111
|
const genAI = new GoogleGenerativeAI(apiKey);
|
|
111
|
-
|
|
112
|
+
// Switch to higher limit model
|
|
113
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash' });
|
|
112
114
|
const instruction = await this.getSystemInstruction();
|
|
113
115
|
|
|
114
116
|
const result = await model.generateContent({
|
|
115
|
-
contents: [
|
|
116
|
-
{ role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] },
|
|
117
|
-
],
|
|
117
|
+
contents: [{ role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] }],
|
|
118
118
|
});
|
|
119
119
|
|
|
120
120
|
const response = await result.response;
|
package/package.json
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "vanguard-cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "AI-Powered Supply Chain Firewall for Git",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"vanguard": "./bin/vanguard.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "vitest run",
|
|
11
|
-
"format": "prettier --write .",
|
|
12
|
-
"lint": "eslint .",
|
|
13
|
-
"release": "release-it",
|
|
14
|
-
"release:dry": "release-it --dry-run"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"eslint
|
|
43
|
-
"eslint-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "vanguard-cli",
|
|
3
|
+
"version": "3.1.2",
|
|
4
|
+
"description": "AI-Powered Supply Chain Firewall for Git",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vanguard": "./bin/vanguard.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"release": "release-it",
|
|
14
|
+
"release:dry": "release-it --dry-run",
|
|
15
|
+
"ship": "node scripts/ship.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "Behram Bazo",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/bazob/vanguard.git"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@google/generative-ai": "^0.21.0",
|
|
26
|
+
"boxen": "^8.0.1",
|
|
27
|
+
"chalk": "^4.1.2",
|
|
28
|
+
"cli-table3": "^0.6.5",
|
|
29
|
+
"commander": "^13.1.0",
|
|
30
|
+
"conf": "^12.0.0",
|
|
31
|
+
"crypto-js": "^4.2.0",
|
|
32
|
+
"figlet": "^1.8.0",
|
|
33
|
+
"inquirer": "^8.2.4",
|
|
34
|
+
"node-fetch": "^2.7.0",
|
|
35
|
+
"ora": "^8.1.1",
|
|
36
|
+
"p-limit": "^7.2.0",
|
|
37
|
+
"simple-git": "^3.27.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^9.39.2",
|
|
41
|
+
"@release-it/conventional-changelog": "^10.0.4",
|
|
42
|
+
"eslint": "^9.39.2",
|
|
43
|
+
"eslint-config-prettier": "^10.1.8",
|
|
44
|
+
"eslint-plugin-node": "^11.1.0",
|
|
45
|
+
"globals": "^17.1.0",
|
|
46
|
+
"nock": "^14.0.10",
|
|
47
|
+
"prettier": "^3.8.1",
|
|
48
|
+
"release-it": "^19.2.4",
|
|
49
|
+
"vitest": "^4.0.18"
|
|
50
|
+
}
|
|
51
|
+
}
|