@vnptnan/gate 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +51 -2
- package/VERSION +1 -0
- package/bin/cli.js +226 -0
- package/ci/.gitlab-ci.yml +41 -0
- package/cli/vnptnan-gate +90 -0
- package/cli/vnptnan-gate.cmd +9 -0
- package/engine/gate-deps.sh +110 -0
- package/engine/gate-lib.sh +285 -0
- package/engine/gate-scan.sh +155 -0
- package/engine/prompt-check.sh +77 -0
- package/gate.conf +13 -0
- package/hooks/commit-msg +10 -0
- package/hooks/pre-commit +7 -0
- package/hooks/pre-push +7 -0
- package/installer/install-server.sh +33 -0
- package/installer/install.ps1 +80 -0
- package/installer/uninstall.ps1 +15 -0
- package/package.json +44 -3
- package/policy.json +4 -0
- package/rules/ai-tools.rules +15 -0
- package/rules/gate.rules +75 -0
- package/rules/packages.rules +18 -0
- package/server/pre-receive +13 -0
- package/tools/lint-rules.sh +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VNPT Nghệ An - Phòng Giải pháp 2
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VNPTNAN_GATE: Cổng kiểm soát ATBMTT khi dùng AI trong phát triển phần mềm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Chặn secret, dữ liệu cá nhân khách hàng, file cấm và package đáng ngờ **trước khi chúng rời máy dev**. Cổng chạy ở năm điểm:
|
|
4
|
+
|
|
5
|
+
| Điểm | Cơ chế |
|
|
6
|
+
|---|---|
|
|
7
|
+
| 0 · Gửi prompt | Hook `UserPromptSubmit` của Claude Code, Codex (`engine/prompt-check.sh`) |
|
|
8
|
+
| 1 · Commit | `pre-commit`: quét các dòng được thêm |
|
|
9
|
+
| 2 · Push | `pre-push`: quét mọi commit sắp rời máy, kể cả commit tạo bằng lệnh plumbing |
|
|
10
|
+
| 3 · Máy chủ GitLab | `pre-receive`: chặn cả máy chưa cài gì, có ở GitLab bản Free, không cần CI |
|
|
11
|
+
| 4 · CI | Mẫu `ci/.gitlab-ci.yml`: quét toàn cây, dependency |
|
|
12
|
+
|
|
13
|
+
Cổng không dùng AI để quyết định. Có 43 rule regex tiếng Việt (`rules/gate.rules`, id `g-`), một engine bash dùng chung cho mọi điểm, log luôn che giá trị secret. Bộ này bổ sung cho VNPTNAN_HARNESS (hook trong phiên AI): hai bên dùng tên hook, thư mục, lệnh và id rule khác nhau.
|
|
14
|
+
|
|
15
|
+
## Cài trên máy dev Windows
|
|
16
|
+
|
|
17
|
+
Cần Git for Windows và Node.js 16.7 trở lên.
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
npx -y @vnptnan/gate # xin quyền admin, cài, tự cập nhật hằng ngày
|
|
21
|
+
vnptnan-gate doctor # mở terminal mới rồi kiểm tra
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Lệnh | Việc |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `npx @vnptnan/gate status` | Bản đang cài, bản mới nhất, chế độ, lịch cập nhật |
|
|
27
|
+
| `npx @vnptnan/gate update` | Cập nhật ngay |
|
|
28
|
+
| `npx @vnptnan/gate --audit` | Cố định chế độ chỉ ghi log trên máy này (mặc định theo `policy.json` của bản phát hành) |
|
|
29
|
+
| `npx @vnptnan/gate uninstall` | Gỡ |
|
|
30
|
+
|
|
31
|
+
Máy không có Node: tải `VNPTNAN_GATE-<ver>-setup.cmd` ở trang Releases, nhấp đúp.
|
|
32
|
+
|
|
33
|
+
Máy chủ GitLab: tải `VNPTNAN_GATE-<ver>.zip` ở trang Releases, giải nén, rồi chạy `sudo bash installer/install-server.sh -mode audit`.
|
|
34
|
+
|
|
35
|
+
## Dùng hằng ngày
|
|
36
|
+
|
|
37
|
+
`vnptnan-gate scan` · `scan tree` · `scan range A B` · `deps` · `test "<dòng>"` · `report [ngày]` · `doctor` · `tools` · `install-hooks`
|
|
38
|
+
|
|
39
|
+
Bị chặn nhầm thì khai ngoại lệ, không dùng `--no-verify`:
|
|
40
|
+
- trên dòng: `gate-allow:<id> <lý do>`;
|
|
41
|
+
- theo đường dẫn: `.vnptnan-gate/allow.rules`, qua merge request có người duyệt.
|
|
42
|
+
|
|
43
|
+
## Phát triển và phát hành
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bash tests/run-tests.sh # 94 ca: rule, hook, pre-receive, prompt
|
|
47
|
+
bash tools/lint-rules.sh
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Mỗi push chạy GitHub Actions (`ci.yml`): kiểm thử trên Linux, rồi cài thật trên Windows và kiểm tra hook chặn secret. Phát hành bằng cách tăng `VERSION` rồi đẩy tag `vX.Y.Z`: `release.yml` chạy lại toàn bộ kiểm thử, `npm publish --provenance` và tạo GitHub Release. Chi tiết trong `npm/README.md`.
|
|
51
|
+
|
|
52
|
+
Giấy phép MIT.
|
package/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.1
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// VNPTNAN_GATE - cài đặt và tự cập nhật qua npm (mặc định npmjs.com; đổi kho bằng npm config @vnptnan:registry hoặc --registry).
|
|
3
|
+
// npx @vnptnan/gate cài hoặc cài lại (xin quyền admin qua UAC), đăng ký tự cập nhật hằng ngày
|
|
4
|
+
// npx @vnptnan/gate --audit|--enforce cài và cố định chế độ trên máy này (mặc định: theo policy.json của gói)
|
|
5
|
+
// npx @vnptnan/gate status phiên bản đang cài, bản mới nhất trên kho, chế độ, lịch cập nhật
|
|
6
|
+
// npx @vnptnan/gate update [--check] cập nhật ngay (lịch tự chạy lệnh này bằng tài khoản SYSTEM)
|
|
7
|
+
// npx @vnptnan/gate uninstall gỡ cổng và lịch cập nhật
|
|
8
|
+
// Tuỳ chọn: --registry URL, --ca FILE (CA khi dùng kho nội bộ tự ký), --token T (khi kho cần đăng nhập).
|
|
9
|
+
// Không phụ thuộc gói ngoài. Cổng chạy bằng bash như trước; Node chỉ dùng để tải, kiểm tra và cài.
|
|
10
|
+
'use strict';
|
|
11
|
+
const fs = require('fs'), path = require('path'), cp = require('child_process'), zlib = require('zlib');
|
|
12
|
+
const crypto = require('crypto'), tls = require('tls'), https = require('https'), http = require('http');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
16
|
+
const IS_WIN = process.platform === 'win32';
|
|
17
|
+
const HOME = path.join(process.env.ProgramData || 'C:\\ProgramData', 'VNPTNAN_GATE');
|
|
18
|
+
const CFG = path.join(HOME, 'update.json');
|
|
19
|
+
const LOG = path.join(HOME, 'logs', 'update.log');
|
|
20
|
+
const TASK = 'VNPTNAN_GATE Update';
|
|
21
|
+
|
|
22
|
+
// ------------------------------------------------------------------ tham số
|
|
23
|
+
const argv = process.argv.slice(2);
|
|
24
|
+
const cmd = argv[0] && !argv[0].startsWith('-') ? argv.shift() : 'setup';
|
|
25
|
+
const opt = {};
|
|
26
|
+
for (let i = 0; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i];
|
|
28
|
+
if (a === '--audit' || a === '--enforce') opt.mode = a.slice(2);
|
|
29
|
+
else if (a === '--check' || a === '--quiet' || a === '--elevated') opt[a.slice(2)] = true;
|
|
30
|
+
else if (['--registry', '--ca', '--token', '--stage-only'].includes(a)) opt[a.slice(2).replace('-o', 'O')] = argv[++i];
|
|
31
|
+
else die(`Tham số không hiểu: ${a}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function say(s) { if (!opt.quiet) console.log(s); log(s); }
|
|
35
|
+
function log(s) { try { fs.mkdirSync(path.dirname(LOG), { recursive: true }); fs.appendFileSync(LOG, `${new Date().toISOString()}\t${s}\n`); } catch (_) { /* chưa có quyền ghi */ } }
|
|
36
|
+
function die(s, code = 1) { console.error(`[VNPTNAN_GATE] ${s}`); log(`LOI\t${s}`); process.exit(code); }
|
|
37
|
+
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
|
|
38
|
+
const cmpVer = (a, b) => { const x = a.split(/[.-]/).map(Number), y = b.split(/[.-]/).map(Number); for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0); return 0; };
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------------ cấu hình kho gói
|
|
41
|
+
function npmConfig(key) { try { return cp.execSync(`npm config get ${key}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch (_) { return ''; } }
|
|
42
|
+
function settings() {
|
|
43
|
+
const saved = readJson(CFG) || {};
|
|
44
|
+
const scope = PKG.name.split('/')[0];
|
|
45
|
+
let registry = opt.registry || saved.registry || npmConfig(`${scope}:registry`);
|
|
46
|
+
if (!registry || registry === 'undefined') registry = PKG.publishConfig && PKG.publishConfig.registry;
|
|
47
|
+
if (!registry) die('Chưa biết địa chỉ kho gói. Chạy lại với --registry <URL Package Registry của GitLab>.');
|
|
48
|
+
const ca = opt.ca || saved.ca || process.env.NODE_EXTRA_CA_CERTS || '';
|
|
49
|
+
return { name: PKG.name, registry: registry.replace(/\/?$/, '/'), ca, token: opt.token || saved.token || '', mode: opt.mode || saved.mode || '' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ------------------------------------------------------------------ HTTP (có CA nội bộ, theo redirect)
|
|
53
|
+
function get(url, s, asJson, hops = 0) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const u = new URL(url), lib = u.protocol === 'http:' ? http : https;
|
|
56
|
+
const o = { headers: { 'User-Agent': `vnptnan-gate/${PKG.version}`, Accept: asJson ? 'application/json' : '*/*' }, timeout: 30000 };
|
|
57
|
+
if (s.ca && fs.existsSync(s.ca)) o.ca = [...tls.rootCertificates, fs.readFileSync(s.ca, 'utf8')];
|
|
58
|
+
if (s.token && u.host === new URL(s.registry).host) o.headers.Authorization = `Bearer ${s.token}`;
|
|
59
|
+
const req = lib.get(u, o, (res) => {
|
|
60
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location && hops < 5) {
|
|
61
|
+
res.resume(); return resolve(get(new URL(res.headers.location, u).href, s, asJson, hops + 1));
|
|
62
|
+
}
|
|
63
|
+
if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode} ${u.href}`)); }
|
|
64
|
+
const chunks = []; res.on('data', (c) => chunks.push(c));
|
|
65
|
+
res.on('end', () => { const b = Buffer.concat(chunks); try { resolve(asJson ? JSON.parse(b.toString('utf8')) : b); } catch (e) { reject(e); } });
|
|
66
|
+
});
|
|
67
|
+
req.on('timeout', () => req.destroy(new Error(`Hết thời gian chờ ${u.host}`))); req.on('error', reject);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function latest(s) {
|
|
71
|
+
const meta = await get(s.registry + s.name.replace('/', '%2f'), s, true);
|
|
72
|
+
const ver = meta['dist-tags'] && meta['dist-tags'].latest;
|
|
73
|
+
if (!ver || !meta.versions || !meta.versions[ver]) throw new Error('Kho gói không có bản latest');
|
|
74
|
+
return { ver, dist: meta.versions[ver].dist };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ------------------------------------------------------------------ tải, kiểm tra toàn vẹn, giải nén
|
|
78
|
+
function untar(buf, dest) {
|
|
79
|
+
let o = 0, n = 0, longName = null, paxPath = null;
|
|
80
|
+
while (o + 512 <= buf.length) {
|
|
81
|
+
const h = buf.subarray(o, o + 512);
|
|
82
|
+
if (h.every((b) => b === 0)) break;
|
|
83
|
+
const str = (a, b) => h.subarray(a, b).toString('utf8').replace(/\0[\s\S]*$/, '');
|
|
84
|
+
const size = parseInt(str(124, 136).trim() || '0', 8), type = String.fromCharCode(h[156] || 48);
|
|
85
|
+
let name = str(345, 500) ? `${str(345, 500)}/${str(0, 100)}` : str(0, 100);
|
|
86
|
+
const data = buf.subarray(o + 512, o + 512 + size); o += 512 + Math.ceil(size / 512) * 512;
|
|
87
|
+
if (type === 'x') { const m = /\d+ path=([^\n]*)\n/.exec(data.toString('utf8')); if (m) paxPath = m[1]; continue; }
|
|
88
|
+
if (type === 'g') continue;
|
|
89
|
+
if (type === 'L') { longName = data.toString('utf8').replace(/\0[\s\S]*$/, ''); continue; }
|
|
90
|
+
if (paxPath) { name = paxPath; paxPath = null; } if (longName) { name = longName; longName = null; }
|
|
91
|
+
name = name.replace(/^package\//, '');
|
|
92
|
+
if (!name || path.isAbsolute(name) || name.split(/[\\/]/).includes('..')) continue; // chặn ghi ra ngoài thư mục
|
|
93
|
+
const p = path.join(dest, name);
|
|
94
|
+
if (type === '5') { fs.mkdirSync(p, { recursive: true }); continue; }
|
|
95
|
+
if (type !== '0' && type !== '7') continue; // bỏ link, thiết bị
|
|
96
|
+
fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, data); n++;
|
|
97
|
+
}
|
|
98
|
+
return n;
|
|
99
|
+
}
|
|
100
|
+
async function fetchTo(s, dist, dest) {
|
|
101
|
+
const tgz = await get(dist.tarball, s, false);
|
|
102
|
+
if (dist.integrity) {
|
|
103
|
+
const [alg, want] = dist.integrity.split(/-(.+)/);
|
|
104
|
+
const got = crypto.createHash(alg).update(tgz).digest('base64');
|
|
105
|
+
if (got !== want) throw new Error(`Sai mã toàn vẹn (${alg}): gói tải về khác gói trên kho, không cài`);
|
|
106
|
+
} else if (dist.shasum) {
|
|
107
|
+
if (crypto.createHash('sha1').update(tgz).digest('hex') !== dist.shasum) throw new Error('Sai shasum, không cài');
|
|
108
|
+
} else throw new Error('Kho gói không cung cấp mã toàn vẹn, không cài');
|
|
109
|
+
fs.rmSync(dest, { recursive: true, force: true }); fs.mkdirSync(dest, { recursive: true });
|
|
110
|
+
const n = untar(zlib.gunzipSync(tgz), dest);
|
|
111
|
+
for (const f of ['VERSION', 'installer/install.ps1', 'engine/gate-lib.sh', 'bin/cli.js']) {
|
|
112
|
+
if (!fs.existsSync(path.join(dest, f))) throw new Error(`Gói thiếu ${f}`);
|
|
113
|
+
}
|
|
114
|
+
return n;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ------------------------------------------------------------------ Windows: quyền admin, cài, lịch
|
|
118
|
+
function isAdmin() { try { cp.execSync('net session', { stdio: 'ignore' }); return true; } catch (_) { return false; } }
|
|
119
|
+
// Windows PowerShell 5.1 gọi từ PowerShell 7 (pwsh) kế thừa PSModulePath của bản 7 và nạp nhầm module
|
|
120
|
+
// (Get-Acl, Start-Process lỗi "module could not be loaded"). Bỏ biến này để 5.1 tự dựng lại đường dẫn mặc định.
|
|
121
|
+
const PS_ENV = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.toLowerCase() !== 'psmodulepath'));
|
|
122
|
+
function ps(args) { return cp.spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', ...args], { stdio: opt.quiet ? 'ignore' : 'inherit', env: PS_ENV }).status; }
|
|
123
|
+
function elevate(extra) {
|
|
124
|
+
if (opt.elevated) die('Đã chạy với quyền nâng nhưng vẫn không có quyền admin (net session lỗi). Mở terminal "Run as administrator" rồi chạy lại.');
|
|
125
|
+
const q = (x) => `'${String(x).replace(/'/g, "''")}'`;
|
|
126
|
+
const args = [__filename, cmd, ...extra, '--elevated'].map((x) => `'"${String(x).replace(/'/g, "''")}"'`).join(',');
|
|
127
|
+
say('Đang xin quyền quản trị (UAC)...');
|
|
128
|
+
const st = cp.spawnSync('powershell.exe', ['-NoProfile', '-Command',
|
|
129
|
+
`$p = Start-Process -FilePath ${q(process.execPath)} -ArgumentList @(${args}) -Verb RunAs -Wait -PassThru; exit $p.ExitCode`], { stdio: 'inherit', env: PS_ENV }).status;
|
|
130
|
+
try { const tail = fs.readFileSync(LOG, 'utf8').trim().split('\n').slice(-6).map((l) => l.split('\t').slice(1).join(' ')); console.log(tail.join('\n')); } catch (_) { /* không có log */ }
|
|
131
|
+
process.exit(st || 0);
|
|
132
|
+
}
|
|
133
|
+
function passArgs(s) {
|
|
134
|
+
const a = ['--registry', s.registry];
|
|
135
|
+
if (s.ca) a.push('--ca', path.resolve(s.ca));
|
|
136
|
+
if (s.token) a.push('--token', s.token);
|
|
137
|
+
if (opt.mode) a.push(`--${opt.mode}`);
|
|
138
|
+
return a;
|
|
139
|
+
}
|
|
140
|
+
function installFrom(dir, mode) {
|
|
141
|
+
const st = ps(['-File', path.join(dir, 'installer', 'install.ps1'), '-Mode', mode]);
|
|
142
|
+
if (st !== 0) throw new Error(`install.ps1 lỗi (mã ${st})`);
|
|
143
|
+
}
|
|
144
|
+
function modeFor(s, dir) {
|
|
145
|
+
if (s.mode) return s.mode; // cố định trên máy này (--audit / --enforce)
|
|
146
|
+
const pol = readJson(path.join(dir, 'policy.json')); // theo chính sách phát hành cùng gói
|
|
147
|
+
return pol && /^(audit|enforce)$/.test(pol.mode) ? pol.mode : 'enforce';
|
|
148
|
+
}
|
|
149
|
+
function saveConfig(s) {
|
|
150
|
+
let ca = '';
|
|
151
|
+
if (s.ca) { ca = path.join(HOME, 'gitlab-ca.crt'); if (path.resolve(s.ca) !== ca) fs.copyFileSync(s.ca, ca); }
|
|
152
|
+
fs.writeFileSync(CFG, JSON.stringify({ registry: s.registry, ca, token: s.token, mode: s.mode }, null, 2));
|
|
153
|
+
}
|
|
154
|
+
function schedule() {
|
|
155
|
+
const tr = `"${process.execPath}" "${path.join(HOME, 'current', 'bin', 'cli.js')}" update --quiet`;
|
|
156
|
+
for (const [suffix, sc] of [['', ['/SC', 'DAILY', '/ST', '10:17']], [' (khoi dong)', ['/SC', 'ONSTART', '/DELAY', '0010:00']]]) {
|
|
157
|
+
const st = cp.spawnSync('schtasks.exe', ['/Create', '/F', '/TN', TASK + suffix, '/RU', 'SYSTEM', '/RL', 'HIGHEST', '/TR', tr, ...sc], { stdio: 'ignore' }).status;
|
|
158
|
+
if (st !== 0) say(`Cảnh báo: không tạo được lịch "${TASK + suffix}" (mã ${st})`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function installedVersion() { try { return fs.readFileSync(path.join(HOME, 'current', 'VERSION'), 'utf8').trim(); } catch (_) { return ''; } }
|
|
162
|
+
|
|
163
|
+
// ------------------------------------------------------------------ lệnh
|
|
164
|
+
async function setup() {
|
|
165
|
+
if (!IS_WIN) die('Lệnh cài này dành cho máy dev Windows. Máy chủ GitLab dùng installer/install-server.sh.');
|
|
166
|
+
const s = settings();
|
|
167
|
+
if (!isAdmin()) return elevate(passArgs(s));
|
|
168
|
+
const stage = path.join(HOME, 'staging', PKG.version);
|
|
169
|
+
fs.rmSync(stage, { recursive: true, force: true }); fs.mkdirSync(stage, { recursive: true });
|
|
170
|
+
fs.cpSync(ROOT, stage, { recursive: true, filter: (p) => !/[\\/]node_modules([\\/]|$)/.test(p) });
|
|
171
|
+
const mode = modeFor(s, stage);
|
|
172
|
+
installFrom(stage, mode);
|
|
173
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
174
|
+
saveConfig(s); schedule();
|
|
175
|
+
say(`Đã cài VNPTNAN_GATE ${PKG.version} (MODE=${mode}); tự cập nhật hằng ngày từ ${s.registry}`);
|
|
176
|
+
say('Mở terminal mới và chạy: vnptnan-gate doctor');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function update() {
|
|
180
|
+
const s = settings();
|
|
181
|
+
const cur = installedVersion();
|
|
182
|
+
const { ver, dist } = await latest(s);
|
|
183
|
+
if (opt.check) { console.log(`Đang cài: ${cur || '(chưa cài)'} · Trên kho: ${ver}${cur && cmpVer(ver, cur) > 0 ? ' · có bản mới' : ''}`); return; }
|
|
184
|
+
if (opt.stageOnly) { const n = await fetchTo(s, dist, path.resolve(opt.stageOnly)); console.log(`Đã tải ${ver}, kiểm tra toàn vẹn đạt, giải nén ${n} file vào ${opt.stageOnly}`); return; }
|
|
185
|
+
if (!IS_WIN) die('Cập nhật chỉ chạy trên máy dev Windows.');
|
|
186
|
+
if (!isAdmin()) return elevate(passArgs(s));
|
|
187
|
+
if (cur && cmpVer(ver, cur) <= 0) {
|
|
188
|
+
// Cùng phiên bản: chỉ đổi chế độ nếu chính sách gói khác chế độ đang chạy
|
|
189
|
+
const conf = (() => { try { return fs.readFileSync(path.join(HOME, 'current', 'gate.conf'), 'utf8'); } catch (_) { return ''; } })();
|
|
190
|
+
const want = modeFor(s, path.join(HOME, 'current')), now = (/^MODE=(\w+)/m.exec(conf) || [])[1];
|
|
191
|
+
if (now && want !== now) { installFrom(path.join(HOME, 'current'), want); say(`Đổi chế độ ${now} -> ${want}`); }
|
|
192
|
+
else say(`Đã là bản mới nhất ${cur}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const stage = path.join(HOME, 'staging', ver);
|
|
196
|
+
const n = await fetchTo(s, dist, stage);
|
|
197
|
+
const mode = modeFor(s, stage);
|
|
198
|
+
installFrom(stage, mode);
|
|
199
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
200
|
+
// Bản mới có thể sửa chính trình cập nhật: đăng ký lại lịch trỏ vào bản vừa cài
|
|
201
|
+
schedule();
|
|
202
|
+
say(`Đã cập nhật ${cur || '(chưa cài)'} -> ${ver} (${n} file, MODE=${mode})`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function status() {
|
|
206
|
+
const s = settings(), saved = readJson(CFG) || {};
|
|
207
|
+
console.log(`Đang cài : ${installedVersion() || '(chưa cài)'}${IS_WIN ? ` tại ${path.join(HOME, 'current')}` : ''}`);
|
|
208
|
+
console.log(`Kho gói : ${s.registry}${s.ca ? ` (CA: ${s.ca})` : ''}`);
|
|
209
|
+
console.log(`Chế độ : ${saved.mode ? `${saved.mode} (cố định trên máy này)` : 'theo policy.json của gói'}`);
|
|
210
|
+
try { const { ver } = await latest(s); console.log(`Bản mới nhất: ${ver}`); } catch (e) { console.log(`Bản mới nhất: không đọc được (${e.message})`); }
|
|
211
|
+
if (IS_WIN) console.log(`Lịch cập nhật: ${cp.spawnSync('schtasks.exe', ['/Query', '/TN', TASK], { stdio: 'ignore' }).status === 0 ? 'có' : 'CHƯA CÓ'}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function uninstall() {
|
|
215
|
+
if (!IS_WIN) die('Chỉ dùng trên máy dev Windows.');
|
|
216
|
+
if (!isAdmin()) return elevate([]);
|
|
217
|
+
for (const t of [TASK, `${TASK} (khoi dong)`]) cp.spawnSync('schtasks.exe', ['/Delete', '/F', '/TN', t], { stdio: 'ignore' });
|
|
218
|
+
const un = path.join(HOME, 'current', 'installer', 'uninstall.ps1');
|
|
219
|
+
if (fs.existsSync(un)) { const tmp = path.join(require('os').tmpdir(), `vnptnan-gate-uninstall-${process.pid}.ps1`); fs.copyFileSync(un, tmp); ps(['-File', tmp]); fs.rmSync(tmp, { force: true }); }
|
|
220
|
+
fs.rmSync(CFG, { force: true });
|
|
221
|
+
say('Đã gỡ VNPTNAN_GATE và lịch tự cập nhật.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const cmds = { setup, update, status, uninstall, version: async () => console.log(PKG.version) };
|
|
225
|
+
if (!cmds[cmd]) die(`Lệnh không có: ${cmd}. Dùng: setup | update [--check] | status | uninstall`);
|
|
226
|
+
cmds[cmd]().catch((e) => die(e.message));
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Mẫu pipeline cho khi GitLab được nâng cấp và bật CI (Harness để "CI" ở mục Để sau; Gate chuẩn bị sẵn).
|
|
2
|
+
# Thêm vào .gitlab-ci.yml của dự án: include: { project: 'devops/vnptnan-gate', file: 'ci/.gitlab-ci.yml' }
|
|
3
|
+
stages: [gate]
|
|
4
|
+
|
|
5
|
+
vnptnan-gate:
|
|
6
|
+
stage: gate
|
|
7
|
+
image: alpine:3.20
|
|
8
|
+
variables:
|
|
9
|
+
GIT_DEPTH: "0"
|
|
10
|
+
GATE_MODE: enforce
|
|
11
|
+
GATE_DEPS: block
|
|
12
|
+
GATE_DEPS_AUDIT: "0" # bật "1" khi runner có mạng tới registry
|
|
13
|
+
before_script:
|
|
14
|
+
- apk add --no-cache bash git grep sed gawk coreutils
|
|
15
|
+
- git clone --depth 1 "$CI_SERVER_URL/devops/vnptnan-gate.git" /opt/vnptnan-gate || cp -r "$CI_PROJECT_DIR/.vnptnan-gate-src" /opt/vnptnan-gate
|
|
16
|
+
script:
|
|
17
|
+
# Merge request: chỉ quét commit của nhánh. Nhánh thường: quét commit mới của lần push.
|
|
18
|
+
- |
|
|
19
|
+
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
|
|
20
|
+
bash /opt/vnptnan-gate/engine/gate-scan.sh range "$CI_MERGE_REQUEST_DIFF_BASE_SHA" "$CI_COMMIT_SHA" "$CI_COMMIT_REF_NAME"
|
|
21
|
+
elif [ "$CI_COMMIT_BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then
|
|
22
|
+
bash /opt/vnptnan-gate/engine/gate-scan.sh range "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" "$CI_COMMIT_REF_NAME"
|
|
23
|
+
else
|
|
24
|
+
bash /opt/vnptnan-gate/engine/gate-scan.sh tree .
|
|
25
|
+
fi
|
|
26
|
+
artifacts:
|
|
27
|
+
when: always
|
|
28
|
+
paths: [gate.log]
|
|
29
|
+
expire_in: 30 days
|
|
30
|
+
rules:
|
|
31
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
32
|
+
- if: $CI_COMMIT_BRANCH
|
|
33
|
+
|
|
34
|
+
# Quét toàn bộ cây hằng đêm (schedule) để bắt vi phạm cũ và dependency có lỗ hổng mới công bố.
|
|
35
|
+
vnptnan-gate-nightly:
|
|
36
|
+
extends: vnptnan-gate
|
|
37
|
+
variables: { GATE_DEPS_AUDIT: "1" }
|
|
38
|
+
script:
|
|
39
|
+
- bash /opt/vnptnan-gate/engine/gate-scan.sh tree .
|
|
40
|
+
rules:
|
|
41
|
+
- if: $CI_PIPELINE_SOURCE == "schedule"
|
package/cli/vnptnan-gate
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Lệnh vnptnan-gate: giao diện dòng lệnh của Cổng kiểm soát ATBMTT.
|
|
3
|
+
set -u
|
|
4
|
+
ROOT=${VNPTNAN_GATE_HOME:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}
|
|
5
|
+
. "$ROOT/engine/gate-lib.sh"
|
|
6
|
+
|
|
7
|
+
usage() {
|
|
8
|
+
cat <<EOF
|
|
9
|
+
vnptnan-gate $GATE_VERSION — Cổng kiểm soát ATBMTT khi dùng AI trong phát triển phần mềm
|
|
10
|
+
|
|
11
|
+
vnptnan-gate scan quét thay đổi đã stage (như pre-commit)
|
|
12
|
+
vnptnan-gate scan tree [thư-mục] quét toàn bộ repo (đánh giá repo cũ trước khi bật cổng)
|
|
13
|
+
vnptnan-gate scan range A B quét các commit A..B (như pre-push)
|
|
14
|
+
vnptnan-gate deps [thư-mục] kiểm dependency và giấy phép
|
|
15
|
+
vnptnan-gate test "<dòng>" xem rule nào khớp một dòng nội dung hoặc một đường dẫn, không ghi log
|
|
16
|
+
vnptnan-gate report [số-ngày] thống kê phát hiện trên máy này (mặc định 7 ngày)
|
|
17
|
+
vnptnan-gate doctor kiểm tra cài đặt: hook, rule, log, cùng tồn tại với VNPTNAN_HARNESS
|
|
18
|
+
vnptnan-gate tools in danh mục công cụ AI được phép
|
|
19
|
+
vnptnan-gate install-hooks cài hook vào .git/hooks của repo hiện tại (khi không cài cấp máy)
|
|
20
|
+
vnptnan-gate version
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
cmd=${1:-help}; shift || true
|
|
25
|
+
case "$cmd" in
|
|
26
|
+
scan)
|
|
27
|
+
sub=${1:-staged}; shift || true
|
|
28
|
+
case "$sub" in
|
|
29
|
+
staged|"") exec bash "$ROOT/engine/gate-scan.sh" staged ;;
|
|
30
|
+
tree) exec bash "$ROOT/engine/gate-scan.sh" tree "${1:-.}" ;;
|
|
31
|
+
range) exec bash "$ROOT/engine/gate-scan.sh" range "$@" ;;
|
|
32
|
+
commit) exec bash "$ROOT/engine/gate-scan.sh" commit "$@" ;;
|
|
33
|
+
*) usage; exit 2 ;;
|
|
34
|
+
esac ;;
|
|
35
|
+
deps) exec bash "$ROOT/engine/gate-scan.sh" deps "${1:-.}" ;;
|
|
36
|
+
test)
|
|
37
|
+
text=${1:-}; [ -n "$text" ] || { usage; exit 2; }
|
|
38
|
+
gate_load_conf ""; gate_load_rules || exit 2
|
|
39
|
+
GATE_LOG=/dev/null; GATE_QUIET=0
|
|
40
|
+
tmp=$(mktemp); printf '%s\n' "$text" > "$tmp"
|
|
41
|
+
echo "Xét như đường dẫn:"; gate_scan_file "$text" /dev/null /dev/null 0
|
|
42
|
+
echo "Xét như một dòng nội dung:"; gate_scan_file "test.txt" "$tmp" "$tmp" ${#text}
|
|
43
|
+
rm -f "$tmp"
|
|
44
|
+
[ "$GATE_N_BLOCK" = 0 ] && [ "$GATE_N_WARN" = 0 ] && echo " không rule nào khớp"
|
|
45
|
+
exit 0 ;;
|
|
46
|
+
report)
|
|
47
|
+
days=${1:-7}; gate_load_conf ""
|
|
48
|
+
[ -f "$GATE_LOG" ] || { echo "Chưa có log tại $GATE_LOG"; exit 0; }
|
|
49
|
+
since=$(date -u -d "-$days days" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-"$days"d +%Y-%m-%dT%H:%M:%SZ)
|
|
50
|
+
echo "Phát hiện trong $days ngày (từ $since), log: $GATE_LOG"
|
|
51
|
+
awk -F'\t' -v s="$since" '$1 >= s' "$GATE_LOG" > "${TMPDIR:-/tmp}/gate-report.$$"
|
|
52
|
+
echo; echo "Theo quyết định:"; awk -F'\t' '{c[$7]++} END{for(k in c) printf " %-16s %d\n", k, c[k]}' "${TMPDIR:-/tmp}/gate-report.$$"
|
|
53
|
+
echo; echo "Theo rule:"; awk -F'\t' '{c[$6" ("$7")"]++} END{for(k in c) printf " %-40s %d\n", k, c[k]}' "${TMPDIR:-/tmp}/gate-report.$$" | sort -k2 -nr
|
|
54
|
+
echo; echo "Theo giai đoạn:"; awk -F'\t' '{c[$4]++} END{for(k in c) printf " %-16s %d\n", k, c[k]}' "${TMPDIR:-/tmp}/gate-report.$$"
|
|
55
|
+
echo; echo "Theo repo:"; awk -F'\t' '{c[$8]++} END{for(k in c) printf " %-30s %d\n", k, c[k]}' "${TMPDIR:-/tmp}/gate-report.$$"
|
|
56
|
+
rm -f "${TMPDIR:-/tmp}/gate-report.$$"; exit 0 ;;
|
|
57
|
+
doctor)
|
|
58
|
+
gate_load_conf ""; ok=1
|
|
59
|
+
echo "VNPTNAN_GATE $GATE_VERSION tại $ROOT"
|
|
60
|
+
echo "Chế độ: MODE=$GATE_MODE DEPS=$GATE_DEPS LOG=$GATE_LOG"
|
|
61
|
+
if gate_load_rules; then echo " [ok] rule: ${#RULE_ID[@]} rule hợp lệ"; else echo " [LOI] rule không nạp được"; ok=0; fi
|
|
62
|
+
for h in commit push; do
|
|
63
|
+
ev=$(git config --system --get "hook.vnptnan-gate-$h.event" 2>/dev/null || git config --global --get "hook.vnptnan-gate-$h.event" 2>/dev/null)
|
|
64
|
+
if [ -n "$ev" ]; then echo " [ok] hook cấp máy: vnptnan-gate-$h -> $ev"
|
|
65
|
+
elif [ -x "$(git rev-parse --git-dir 2>/dev/null)/hooks/pre-$h" ] 2>/dev/null && grep -q vnptnan-gate "$(git rev-parse --git-dir)/hooks/pre-$h"; then echo " [ok] hook của repo: pre-$h"
|
|
66
|
+
else echo " [THIEU] hook pre-$h chưa cài (chạy installer/install.ps1 hoặc vnptnan-gate install-hooks)"; ok=0; fi
|
|
67
|
+
done
|
|
68
|
+
if git config --system --get hook.vnptnan-guard.event >/dev/null 2>&1; then
|
|
69
|
+
echo " [ok] VNPTNAN_HARNESS pre-push guard có mặt; hai hook dùng tên khác nhau, chạy nối tiếp"
|
|
70
|
+
else echo " [i] VNPTNAN_HARNESS chưa cài trên máy này (không bắt buộc)"; fi
|
|
71
|
+
printf 'AKIAIOSFODNN7EXAMPLE\n' > "${TMPDIR:-/tmp}/gate-doctor.$$"
|
|
72
|
+
GATE_LOG=/dev/null GATE_QUIET=1 gate_scan_file probe.txt "${TMPDIR:-/tmp}/gate-doctor.$$" "${TMPDIR:-/tmp}/gate-doctor.$$" 21 2>/dev/null
|
|
73
|
+
rm -f "${TMPDIR:-/tmp}/gate-doctor.$$"
|
|
74
|
+
if [ "$GATE_N_BLOCK" -gt 0 ]; then echo " [ok] engine phát hiện được secret mẫu"; else echo " [LOI] engine không phát hiện được secret mẫu"; ok=0; fi
|
|
75
|
+
d=$(dirname "$GATE_LOG"); mkdir -p "$d" 2>/dev/null && touch "$GATE_LOG" 2>/dev/null && echo " [ok] ghi log được" || { echo " [LOI] không ghi được log $GATE_LOG"; ok=0; }
|
|
76
|
+
[ "$ok" = 1 ] && echo "all good" || { echo "có lỗi"; exit 1; } ;;
|
|
77
|
+
tools)
|
|
78
|
+
awk -F'\t' '!/^#/ && NF>=3 {printf " %-28s %-8s %-6s %s\n", $1, $2, $4, $5}' "$ROOT/rules/ai-tools.rules" ;;
|
|
79
|
+
install-hooks)
|
|
80
|
+
gd=$(git rev-parse --git-dir 2>/dev/null) || { echo "Không ở trong repo git"; exit 2; }
|
|
81
|
+
for h in pre-commit pre-push commit-msg; do
|
|
82
|
+
if [ -f "$gd/hooks/$h" ] && ! grep -q vnptnan-gate "$gd/hooks/$h"; then mv "$gd/hooks/$h" "$gd/hooks/$h.local"; fi
|
|
83
|
+
{ printf '#!/bin/sh\n# vnptnan-gate (cài bằng vnptnan-gate install-hooks)\n'
|
|
84
|
+
printf '[ -x "$0.local" ] && { "$0.local" "$@" || exit $?; }\n'
|
|
85
|
+
printf 'VNPTNAN_GATE_HOME="%s" exec sh "%s/hooks/%s" "$@"\n' "$ROOT" "$ROOT" "$h"; } > "$gd/hooks/$h"
|
|
86
|
+
chmod +x "$gd/hooks/$h"; echo " đã cài $gd/hooks/$h"
|
|
87
|
+
done ;;
|
|
88
|
+
version|--version|-v) echo "VNPTNAN_GATE $GATE_VERSION" ;;
|
|
89
|
+
help|-h|--help|*) usage ;;
|
|
90
|
+
esac
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
rem Lệnh vnptnan-gate trên Windows: chạy bản bash qua Git Bash.
|
|
3
|
+
setlocal
|
|
4
|
+
set "ROOT=%~dp0.."
|
|
5
|
+
set "BASH=%ProgramFiles%\Git\bin\bash.exe"
|
|
6
|
+
if not exist "%BASH%" set "BASH=%LOCALAPPDATA%\Programs\Git\bin\bash.exe"
|
|
7
|
+
if not exist "%BASH%" ( echo Can Git for Windows. & exit /b 2 )
|
|
8
|
+
"%BASH%" "%ROOT%\cli\vnptnan-gate" %*
|
|
9
|
+
exit /b %ERRORLEVEL%
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Kiểm tra thành phần bên thứ ba (dependency) và giấy phép, chạy offline trên máy dev.
|
|
3
|
+
# Nguồn: package.json + node_modules (npm), *.csproj + cache NuGet (~/.nuget/packages). Rule: rules/packages.rules.
|
|
4
|
+
# Được gate-scan.sh nạp (source), dùng các hàm của gate-lib.sh.
|
|
5
|
+
|
|
6
|
+
PKG_TYPE=(); PKG_LEVEL=(); PKG_RE=(); PKG_MSG=()
|
|
7
|
+
|
|
8
|
+
deps_load_rules() {
|
|
9
|
+
local file=$GATE_ROOT/rules/packages.rules line t l re msg
|
|
10
|
+
[ -f "$file" ] || return 0
|
|
11
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
12
|
+
line=${line%$'\r'}
|
|
13
|
+
[ -z "$line" ] && continue
|
|
14
|
+
case "$line" in '#'*) continue ;; esac
|
|
15
|
+
IFS=$'\t' read -r t l re msg <<< "$line"
|
|
16
|
+
[ -n "$t" ] && [ -n "$l" ] && [ -n "$re" ] || continue
|
|
17
|
+
PKG_TYPE+=("$t"); PKG_LEVEL+=("$l"); PKG_RE+=("$re"); PKG_MSG+=("${msg:-}")
|
|
18
|
+
done < "$file"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
deps_level() {
|
|
22
|
+
# Mức của rule, hạ xuống warn nếu GATE_DEPS=warn.
|
|
23
|
+
if [ "$GATE_DEPS" = warn ]; then echo warn; else echo "$1"; fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
deps_check_one() {
|
|
27
|
+
# $1 hệ (npm|nuget), $2 tên, $3 phiên bản, $4 giấy phép ("" nếu không rõ), $5 đường dẫn file khai báo
|
|
28
|
+
local eco=$1 name=$2 ver=$3 lic=$4 src=$5 i
|
|
29
|
+
for i in "${!PKG_TYPE[@]}"; do
|
|
30
|
+
case "${PKG_TYPE[$i]}" in
|
|
31
|
+
deny-package)
|
|
32
|
+
if printf '%s@%s' "$name" "$ver" | grep -qiE -- "${PKG_RE[$i]}" || printf '%s' "$name" | grep -qiE -- "${PKG_RE[$i]}"; then
|
|
33
|
+
gate_report_finding "$(deps_level "${PKG_LEVEL[$i]}")" "g-dep-package" "$src" "" "$eco $name@$ver" "${PKG_MSG[$i]}"
|
|
34
|
+
fi ;;
|
|
35
|
+
deny-license)
|
|
36
|
+
if printf '%s\n' "$lic" | grep -qiE -- "${PKG_RE[$i]}"; then
|
|
37
|
+
gate_report_finding "$(deps_level "${PKG_LEVEL[$i]}")" "g-dep-license" "$src" "" "$eco $name@$ver license=${lic:-?}" "${PKG_MSG[$i]}"
|
|
38
|
+
fi ;;
|
|
39
|
+
esac
|
|
40
|
+
done
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deps_json_field() {
|
|
44
|
+
# Lấy giá trị chuỗi của khoá cấp 1 trong file JSON nhỏ (đủ cho package.json). $1 file, $2 khoá.
|
|
45
|
+
tr -d '\n\r' < "$1" | sed 's/[{},]/&\n/g' | sed -n 's/^[[:space:]]*"'"$2"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
deps_npm() {
|
|
49
|
+
local dir=$1 pj name ver lic nm block
|
|
50
|
+
while IFS= read -r pj; do
|
|
51
|
+
[ -n "$pj" ] || continue
|
|
52
|
+
nm=$(dirname "$pj")/node_modules
|
|
53
|
+
# Lấy tên các dependency trực tiếp (dependencies + devDependencies): dòng dạng "ten": "ver" nằm giữa hai khối.
|
|
54
|
+
# Chuẩn hoá JSON về mỗi phần tử một dòng (package.json có thể nằm trên một dòng) rồi lấy khối dependencies.
|
|
55
|
+
block=$(tr -d '\n\r' < "$pj" | sed 's/[{},]/&\n/g' | awk '/"(dependencies|devDependencies|optionalDependencies)"[[:space:]]*:/{f=1;next} f&&/^[[:space:]]*}/{f=0} f')
|
|
56
|
+
while IFS= read -r line; do
|
|
57
|
+
name=$(printf '%s' "$line" | sed -n 's/^[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
58
|
+
[ -n "$name" ] || continue
|
|
59
|
+
if [ -f "$nm/$name/package.json" ]; then
|
|
60
|
+
ver=$(deps_json_field "$nm/$name/package.json" version)
|
|
61
|
+
lic=$(deps_json_field "$nm/$name/package.json" license)
|
|
62
|
+
[ -z "$lic" ] && lic=$(tr -d '\n\r' < "$nm/$name/package.json" | sed -n 's/.*"license"[[:space:]]*:[[:space:]]*{[^}]*"type"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
|
63
|
+
deps_check_one npm "$name" "${ver:-?}" "$lic" "${pj#"$dir"/}"
|
|
64
|
+
else
|
|
65
|
+
ver=$(printf '%s' "$line" | sed -n 's/^[^:]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
66
|
+
deps_check_one npm "$name" "${ver:-?}" "" "${pj#"$dir"/}"
|
|
67
|
+
gate_report_finding "$(deps_level warn)" "g-dep-unresolved" "${pj#"$dir"/}" "" "npm $name@${ver:-?} chưa có trong node_modules" "Package chưa được cài: kiểm tra tên có thật trên registry (AI hay bịa tên package) rồi npm install trước khi push."
|
|
68
|
+
fi
|
|
69
|
+
done <<< "$block"
|
|
70
|
+
if [ "$GATE_DEPS_AUDIT" = 1 ] && command -v npm >/dev/null 2>&1 && [ -d "$nm" ]; then
|
|
71
|
+
if ! (cd "$(dirname "$pj")" && npm audit --audit-level=high >/dev/null 2>&1); then
|
|
72
|
+
gate_report_finding "$(deps_level block)" "g-dep-vulnerable" "${pj#"$dir"/}" "" "npm audit --audit-level=high" "Có lỗ hổng mức high/critical trong dependency npm."
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
done < <(find "$dir" -name package.json -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
deps_nuget() {
|
|
79
|
+
local dir=$1 proj name ver lic cache lower nuspec
|
|
80
|
+
cache=${NUGET_PACKAGES:-${USERPROFILE:-$HOME}/.nuget/packages}
|
|
81
|
+
while IFS= read -r proj; do
|
|
82
|
+
[ -n "$proj" ] || continue
|
|
83
|
+
while IFS='|' read -r name ver; do
|
|
84
|
+
[ -n "$name" ] || continue
|
|
85
|
+
lower=$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')
|
|
86
|
+
lic=""
|
|
87
|
+
nuspec=$(ls "$cache/$lower/$ver/$lower.nuspec" 2>/dev/null | head -n1)
|
|
88
|
+
if [ -n "$nuspec" ]; then
|
|
89
|
+
lic=$(sed -n 's/.*<license[^>]*type="expression"[^>]*>\([^<]*\)<.*/\1/p' "$nuspec" | head -n1)
|
|
90
|
+
[ -z "$lic" ] && grep -q '<licenseUrl>' "$nuspec" && lic="SEE LICENSE"
|
|
91
|
+
deps_check_one nuget "$name" "$ver" "$lic" "${proj#"$dir"/}"
|
|
92
|
+
else
|
|
93
|
+
deps_check_one nuget "$name" "$ver" "" "${proj#"$dir"/}"
|
|
94
|
+
gate_report_finding "$(deps_level warn)" "g-dep-unresolved" "${proj#"$dir"/}" "" "nuget $name@$ver chưa có trong cache NuGet" "Package chưa được restore: kiểm tra tên có thật trên nuget.org (AI hay bịa tên package) rồi dotnet restore trước khi push."
|
|
95
|
+
fi
|
|
96
|
+
done < <(sed -n 's/.*<PackageReference[^>]*Include="\([^"]*\)"[^>]*Version="\([^"]*\)".*/\1|\2/p' "$proj")
|
|
97
|
+
if [ "$GATE_DEPS_AUDIT" = 1 ] && command -v dotnet >/dev/null 2>&1; then
|
|
98
|
+
if dotnet list "$proj" package --vulnerable --include-transitive 2>/dev/null | grep -qiE 'high|critical'; then
|
|
99
|
+
gate_report_finding "$(deps_level block)" "g-dep-vulnerable" "${proj#"$dir"/}" "" "dotnet list package --vulnerable" "Có lỗ hổng mức high/critical trong package NuGet."
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
done < <(find "$dir" -name '*.csproj' -not -path '*/.git/*' 2>/dev/null)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
gate_deps_scan() {
|
|
106
|
+
local dir=${1:-.}
|
|
107
|
+
deps_load_rules
|
|
108
|
+
deps_npm "$dir"
|
|
109
|
+
deps_nuget "$dir"
|
|
110
|
+
}
|