vanguard-cli 3.0.0 â 3.1.3
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 +28 -0
- package/lib/commands/config.js +2 -2
- package/lib/commands/scan.js +23 -9
- package/lib/services/scanner.js +55 -50
- package/package.json +51 -50
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.1.3 (2026-01-24)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* enterprise-grade AI resilience with gemini-1.5-flash-latest đĄď¸âď¸đ⨠([3ab3f1d](https://github.com/bazobehram/vanguard/commit/3ab3f1da4866c3e8bb833efd039f43b1dd6e8942))
|
|
9
|
+
* implement scanner resilience and add test suite đĄď¸đ§Şđ§Źâ¨ ([20310c3](https://github.com/bazobehram/vanguard/commit/20310c30d2787cbc2dedb0a19d8d2a5d2b308d0d))
|
|
10
|
+
* implement smart throttling and robust 429 recovery logic đĄď¸âď¸â¨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
|
|
11
|
+
* initial public release of Vanguard V3 Enterprise Edition ��ď¸đ⨠([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
|
|
12
|
+
|
|
13
|
+
## 3.1.2 (2026-01-24)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* implement scanner resilience and add test suite đĄď¸đ§Şđ§Źâ¨ ([20310c3](https://github.com/bazobehram/vanguard/commit/20310c30d2787cbc2dedb0a19d8d2a5d2b308d0d))
|
|
19
|
+
* implement smart throttling and robust 429 recovery logic đĄď¸âď¸â¨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
|
|
20
|
+
* initial public release of Vanguard V3 Enterprise Edition ��ď¸đ⨠([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
|
|
21
|
+
|
|
22
|
+
## 3.1.1 (2026-01-24)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* implement smart throttling and robust 429 recovery logic đĄď¸âď¸â¨ ([4123794](https://github.com/bazobehram/vanguard/commit/4123794b1cced382fb628a1b828a466d83c24a3e))
|
|
28
|
+
* initial public release of Vanguard V3 Enterprise Edition ��ď¸đ⨠([2335c1a](https://github.com/bazobehram/vanguard/commit/2335c1aa78a95a871cc600e62dd45431147088b9))
|
package/lib/commands/config.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function runConfigWizard() {
|
|
|
14
14
|
name: 'providerSelection',
|
|
15
15
|
message: 'Select AI Provider:',
|
|
16
16
|
choices: [
|
|
17
|
-
{ name: 'Google Gemini
|
|
17
|
+
{ name: 'Google Gemini (Cloud - Reliable & Smart)', value: 'gemini' },
|
|
18
18
|
{ name: 'Local Ollama (Private - Offline)', value: 'ollama' },
|
|
19
19
|
],
|
|
20
20
|
},
|
|
@@ -33,7 +33,7 @@ export async function runConfigWizard() {
|
|
|
33
33
|
},
|
|
34
34
|
]);
|
|
35
35
|
config.set('GEMINI_KEY', apiKey);
|
|
36
|
-
console.log('â
Gemini
|
|
36
|
+
console.log('â
Gemini configured and saved.');
|
|
37
37
|
} else {
|
|
38
38
|
const spinner = createSpinner('đ Detecting local Ollama models...').start();
|
|
39
39
|
try {
|
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,64 @@ ${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 =
|
|
81
|
+
error.message.includes('429') ||
|
|
82
|
+
error.message.includes('Too Many Requests') ||
|
|
83
|
+
error.message.includes('Quota');
|
|
84
|
+
|
|
85
|
+
const isServerErr = error.message.includes('500') || error.message.includes('503');
|
|
86
|
+
|
|
87
|
+
if (isRateLimit && attempt <= maxAttempts) {
|
|
88
|
+
const waitTime = attempt === 1 ? 30 : 60; // Increase wait on second failure
|
|
89
|
+
if (spinner) {
|
|
90
|
+
const originalText = spinner.text;
|
|
91
|
+
for (let i = waitTime; i > 0; i--) {
|
|
92
|
+
spinner.text = `âď¸ Quota hit. Cooling down API (${i}s remaining)...`;
|
|
93
|
+
await sleep(1000);
|
|
94
|
+
}
|
|
95
|
+
spinner.text = originalText;
|
|
88
96
|
} 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;
|
|
97
|
+
console.log(chalk.yellow(`\nâ ď¸ Rate limit hit. Cooling down for ${waitTime}s (Attempt ${attempt}/${maxAttempts})...`));
|
|
98
|
+
await sleep(waitTime * 1000);
|
|
100
99
|
}
|
|
101
|
-
|
|
100
|
+
return this.scanWithRetry(filePath, content, spinner, attempt + 1);
|
|
102
101
|
}
|
|
102
|
+
|
|
103
|
+
if (isServerErr && attempt === 1) {
|
|
104
|
+
if (spinner) spinner.text = 'đ AI Server hiccup. Retrying immediately...';
|
|
105
|
+
return this.scanWithRetry(filePath, content, spinner, attempt + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw error;
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -108,13 +114,12 @@ RESPONSE FORMAT (JSON ONLY):
|
|
|
108
114
|
if (!apiKey) throw new Error('Gemini API Key missing. Run "vanguard config"');
|
|
109
115
|
|
|
110
116
|
const genAI = new GoogleGenerativeAI(apiKey);
|
|
111
|
-
|
|
117
|
+
// Use the -latest alias for better cross-region compatibility
|
|
118
|
+
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-flash-latest' });
|
|
112
119
|
const instruction = await this.getSystemInstruction();
|
|
113
120
|
|
|
114
121
|
const result = await model.generateContent({
|
|
115
|
-
contents: [
|
|
116
|
-
{ role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] },
|
|
117
|
-
],
|
|
122
|
+
contents: [{ role: 'user', parts: [{ text: `${instruction}\n\nFILE CONTENT:\n${content}` }] }],
|
|
118
123
|
});
|
|
119
124
|
|
|
120
125
|
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.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
|
+
"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
|
+
}
|