@hackaiz/cli 0.0.0-stage → 0.1.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 +36 -3
- package/bin/check.mjs +59 -0
- package/bin/gate.mjs +6 -0
- package/bin/hackaiz.mjs +96 -0
- package/bin/hook.mjs +47 -0
- package/package.json +32 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HACKTUALIZ Inc.
|
|
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,36 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Hackaiz CLI
|
|
2
|
+
|
|
3
|
+
Command-line client for [Hackaiz](https://hackaiz.com). Requires Node.js 22 or later.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i -g @hackaiz/cli
|
|
7
|
+
export HACKAIZ_API_KEY='your personal key'
|
|
8
|
+
hackaiz analyze PLATFORM_UUID STABLE_REQUEST_KEY
|
|
9
|
+
hackaiz status PLATFORM_UUID REQUEST_UUID
|
|
10
|
+
hackaiz verify PLATFORM_UUID REQUEST_UUID
|
|
11
|
+
hackaiz report PLATFORM_UUID
|
|
12
|
+
hackaiz findings PLATFORM_UUID
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Your key must allow read and launch access for the platform. Analyses are passive only, require current proof of ownership and consent, and use credits as usual. The CLI never publishes fixes and never merges anything. It requests an analysis, then checks its status; you don't need to click anything in Hackaiz. If your connection drops, check the same request again instead of creating a new one. An analysis that has already started is never replayed automatically after an interruption. A partial analysis does not prove full coverage.
|
|
16
|
+
|
|
17
|
+
Never store your key in a repository or in logs.
|
|
18
|
+
|
|
19
|
+
`verify` reads only the report linked to that request. It needs a complete analysis and reads all of its findings. Exit codes: `0` = no unresolved High/Critical finding, `1` = such findings exist, `2` = incomplete or unavailable. This checks the saved platform; it does not verify a specific PR commit or deployment.
|
|
20
|
+
|
|
21
|
+
## Check before installing
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
hackaiz check ./my-skill # Skill folder, MCP config, package.json, workflow or Dockerfile
|
|
25
|
+
hackaiz check . --sarif > hackaiz.sarif # CI: exit code 2 on a Danger verdict
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Claude Code hook (automatic check before a Skill, MCP or install command)
|
|
29
|
+
|
|
30
|
+
Add to `.claude/settings.json` (with `HACKAIZ_API_KEY` set in your environment):
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit|MultiEdit|Bash", "hooks": [ { "type": "command", "command": "hackaiz hook claude" } ] } ] } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Only Skill/MCP/package/workflow writes and install commands are checked, and each new check uses credits, including unchanged content. Enabling this hook opts into paid checks before these actions; it is not background monitoring. A Danger verdict blocks the action. An incomplete response, missing key, service error or exhausted credits also blocks it: no missing verdict is treated as approval. Set `HACKAIZ_HOOK_FAIL_OPEN=1` only to explicitly choose advisory mode during failures. This hook checks submitted text, not all installed package code or runtime behaviour.
|
package/bin/check.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// `hackaiz check <path> [--sarif]`: static check of a Skill / MCP / package / workflow before installing.
|
|
2
|
+
// Exit 2 on a Danger verdict so CI can block. Secret, binary and generated files are never sent.
|
|
3
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { join, relative, basename } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const SKIP_DIR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.venv', 'venv', '__pycache__']);
|
|
8
|
+
const SECRET = /(^|\/)(\.env(\..*)?|.*\.(pem|key|p12|pfx|keystore|jks)|id_rsa|id_ed25519|\.npmrc|\.netrc)$/i;
|
|
9
|
+
const BINARY = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|tgz|exe|dll|so|dylib|wasm|woff2?|ttf|mp[34]|lockb|bin)$/i;
|
|
10
|
+
|
|
11
|
+
export async function collectComponent(root, { maxFiles = 400, maxFileBytes = 400 * 1024 } = {}) {
|
|
12
|
+
const files = []; const skipped = [];
|
|
13
|
+
const s = await stat(root);
|
|
14
|
+
const walk = async (dir) => {
|
|
15
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
16
|
+
if (files.length >= maxFiles) return;
|
|
17
|
+
const full = join(dir, e.name);
|
|
18
|
+
if (e.isDirectory()) { if (!SKIP_DIR.has(e.name)) await walk(full); continue; }
|
|
19
|
+
const rel = relative(root, full).replace(/\\/g, '/');
|
|
20
|
+
const size = (await stat(full)).size;
|
|
21
|
+
if (SECRET.test(rel) || BINARY.test(rel) || size > maxFileBytes) { skipped.push(rel); continue; }
|
|
22
|
+
const content = await readFile(full, 'utf8');
|
|
23
|
+
if (content.includes('\u0000')) { skipped.push(rel); continue; }
|
|
24
|
+
files.push({ path: rel, content });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (s.isDirectory()) await walk(root);
|
|
28
|
+
else if (!SECRET.test(root)) files.push({ path: basename(root), content: await readFile(root, 'utf8') });
|
|
29
|
+
return { files, skipped };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function toSarif(result) {
|
|
33
|
+
const level = (s) => (s === 'Critical' || s === 'High' ? 'error' : s === 'Medium' ? 'warning' : 'note');
|
|
34
|
+
return {
|
|
35
|
+
version: '2.1.0', $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
36
|
+
runs: [{
|
|
37
|
+
tool: { driver: { name: 'Hackaiz Import Gate', informationUri: 'https://hackaiz.com/check', rules: [...new Map((result.findings ?? []).map((f) => [f.rule, { id: f.rule, shortDescription: { text: f.title }, help: { text: f.action }, properties: { cwe: f.cwe, owasp: f.owasp } }])).values()] } },
|
|
38
|
+
results: (result.findings ?? []).map((f) => ({ ruleId: f.rule, level: level(f.severity), message: { text: `${f.title}: ${f.impact}` }, locations: [{ physicalLocation: { artifactLocation: { uri: f.file }, region: { startLine: f.line } } }] })),
|
|
39
|
+
}],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function checkCommand(target, flags, { env, fetcher, output }) {
|
|
44
|
+
if (!target) throw Error('Usage: hackaiz check <path> [--sarif] [--lang fr]');
|
|
45
|
+
const origin = new URL(env.HACKAIZ_URL ?? 'https://hackaiz.com');
|
|
46
|
+
if (origin.protocol !== 'https:' || origin.pathname !== '/' || origin.username) throw Error('HACKAIZ_URL must be an HTTPS origin');
|
|
47
|
+
if (!/^hkz_[A-Za-z0-9_-]{40,}$/.test(env.HACKAIZ_API_KEY ?? '')) throw Error('HACKAIZ_API_KEY is required');
|
|
48
|
+
const { files } = await collectComponent(target);
|
|
49
|
+
if (!files.length) throw Error('Nothing to check (secret, binary and large files are never sent)');
|
|
50
|
+
const lang = flags.includes('--lang') && flags[flags.indexOf('--lang') + 1] === 'fr' ? 'fr' : 'en';
|
|
51
|
+
const response = await fetcher(`${origin.origin}/api/public/v1/component-checks`, {
|
|
52
|
+
method: 'POST', redirect: 'error', headers: { authorization: `Bearer ${env.HACKAIZ_API_KEY}`, 'content-type': 'application/json' },
|
|
53
|
+
body: JSON.stringify({ request_id: randomUUID(), channel: env.GITHUB_ACTIONS === 'true' ? 'ci' : 'cli', name: basename(target).slice(0, 120) || 'component', language: lang, files }),
|
|
54
|
+
});
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
if (!response.ok) throw Error(`Hackaiz ${response.status}: ${data.error ?? 'request_failed'}`);
|
|
57
|
+
output(JSON.stringify(flags.includes('--sarif') ? toSarif(data) : data, null, 2));
|
|
58
|
+
return data.verdict === 'danger' ? 2 : 0;
|
|
59
|
+
}
|
package/bin/gate.mjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** A gate refers to one saved-platform run, never to an inferred PR commit. */
|
|
2
|
+
export function savedRunGate(analysis, findings) {
|
|
3
|
+
if (analysis?.status !== 'success') return { verdict: 'inconclusive', reason: 'analysis_not_complete', exitCode: 2 };
|
|
4
|
+
const dangerous = findings.filter(f => ['critical', 'high'].includes(String(f.severity).toLowerCase()) && f.status !== 'resolved');
|
|
5
|
+
return dangerous.length ? { verdict: 'fail', unresolvedHighOrCritical: dangerous.length, exitCode: 1 } : { verdict: 'pass', unresolvedHighOrCritical: 0, exitCode: 0 };
|
|
6
|
+
}
|
package/bin/hackaiz.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { savedRunGate } from './gate.mjs';
|
|
5
|
+
|
|
6
|
+
export async function run(args, { env = process.env, fetcher = fetch, output = console.log, stdin, errorOut, wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)) } = {}) {
|
|
7
|
+
const [command, projectId, value] = args;
|
|
8
|
+
if (command === 'help' || !command) {
|
|
9
|
+
output('hackaiz analyze <project-id> [request-key]\nhackaiz status <project-id> <run-id>\nhackaiz verify <project-id> <run-id>\nhackaiz report <project-id>\nhackaiz findings <project-id>\nhackaiz check <path> [--sarif] [--lang fr] (Skill/MCP/package/workflow before installing; exit 2 on Danger)\nhackaiz hook claude (Claude Code PreToolUse hook: blocks Danger Skills/MCP/installs)\nHACKAIZ_API_KEY: personal key. HACKAIZ_URL: optional HTTPS origin.');
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
if (command === 'hook') {
|
|
13
|
+
const { hookCommand } = await import('./hook.mjs');
|
|
14
|
+
const chunks = []; for await (const c of (stdin ?? process.stdin)) chunks.push(c);
|
|
15
|
+
return hookCommand({ env, fetcher, stdin: Buffer.concat(chunks.map((c) => Buffer.from(c))).toString('utf8'), errorOut: errorOut ?? console.error });
|
|
16
|
+
}
|
|
17
|
+
if (command === 'check') {
|
|
18
|
+
const { checkCommand } = await import('./check.mjs');
|
|
19
|
+
return checkCommand(projectId, args.slice(2), { env, fetcher, output });
|
|
20
|
+
}
|
|
21
|
+
if (!['analyze', 'status', 'verify', 'report', 'findings'].includes(command)) throw Error('Unsupported command');
|
|
22
|
+
if (!/^[0-9a-f-]{36}$/i.test(projectId ?? '')) throw Error('A platform UUID is required');
|
|
23
|
+
const origin = new URL(env.HACKAIZ_URL ?? 'https://hackaiz.com');
|
|
24
|
+
if (origin.protocol !== 'https:' || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) throw Error('HACKAIZ_URL must be an HTTPS origin');
|
|
25
|
+
if (!/^hkz_[A-Za-z0-9_-]{40,}$/.test(env.HACKAIZ_API_KEY ?? '')) throw Error('HACKAIZ_API_KEY is required');
|
|
26
|
+
const base = `${origin.origin}/api/public/v1/projects/${projectId}`;
|
|
27
|
+
const call = async (path, body) => {
|
|
28
|
+
const response = await fetcher(`${base}/${path}`, { method: body ? 'POST' : 'GET', headers: { authorization: `Bearer ${env.HACKAIZ_API_KEY}`, 'content-type': 'application/json' }, ...(body ? { body: JSON.stringify(body) } : {}), redirect: 'error' });
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
if (!response.ok) throw Error(`Hackaiz ${response.status}: ${data.error ?? 'request_failed'}${data.purchaseUrl ? ` — ${data.purchaseUrl}` : ''}`);
|
|
31
|
+
return data;
|
|
32
|
+
};
|
|
33
|
+
if (command === 'analyze') {
|
|
34
|
+
const requestKey = value ?? randomUUID();
|
|
35
|
+
const accepted = await call('analysis-requests', { request_key: requestKey, channel: env.GITHUB_ACTIONS === 'true' ? 'ci' : 'cli' });
|
|
36
|
+
if (!/^[0-9a-f-]{36}$/i.test(accepted.requestId ?? '')) throw Error('Invalid analysis response');
|
|
37
|
+
output(JSON.stringify({ requestId: accepted.requestId, requestKey, status: accepted.status }));
|
|
38
|
+
if (accepted.delivery === 'configuration_required') throw Error('Autonomous processing is not configured; keep this request ID');
|
|
39
|
+
// GET-only polling. Closing the client does not cancel autonomous work.
|
|
40
|
+
// A lost response never creates another run or invokes execution again.
|
|
41
|
+
for (let attempt = 0; attempt < 360; attempt++) {
|
|
42
|
+
const result = await call(`analysis-runs/${accepted.requestId}`);
|
|
43
|
+
const status = result.request?.status;
|
|
44
|
+
if (['success', 'partial', 'failed', 'cancelled'].includes(status)) {
|
|
45
|
+
output(JSON.stringify(result));
|
|
46
|
+
return ['success', 'partial'].includes(status) ? 0 : 2;
|
|
47
|
+
}
|
|
48
|
+
await wait(5000);
|
|
49
|
+
}
|
|
50
|
+
output(JSON.stringify({ requestId: accepted.requestId, status: 'still_pending', note: 'Use status with the same request ID; do not create a replacement.' }));
|
|
51
|
+
return 2;
|
|
52
|
+
}
|
|
53
|
+
if (command === 'status') {
|
|
54
|
+
if (!/^[0-9a-f-]{36}$/i.test(value ?? '')) throw Error('A run UUID is required');
|
|
55
|
+
output(JSON.stringify(await call(`analysis-runs/${value}`))); return 0;
|
|
56
|
+
}
|
|
57
|
+
if (command === 'verify') {
|
|
58
|
+
if (!/^[0-9a-f-]{36}$/i.test(value ?? '')) throw Error('A run UUID is required');
|
|
59
|
+
const status = await call(`analysis-runs/${value}`);
|
|
60
|
+
const scan = status.analysis;
|
|
61
|
+
if (!scan || !/^[0-9a-f-]{36}$/i.test(scan.id ?? '') || status.request?.scan_id !== scan.id || scan.status !== 'success') {
|
|
62
|
+
output(JSON.stringify({ verdict: 'inconclusive', reason: 'analysis_not_complete_or_unlinked', runId: value }));
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
const findings = []; const seen = new Set(); let offset = 0; let total;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const result = await call(`findings?scanId=${scan.id}&status=open&limit=100&offset=${offset}`);
|
|
68
|
+
const invalid = !Array.isArray(result.data) || !Number.isSafeInteger(result.total) || result.total < 0 || result.total > 100000 || (total !== undefined && result.total !== total)
|
|
69
|
+
|| result.data.length > 100 || offset + result.data.length > result.total || (!result.data.length && offset < result.total)
|
|
70
|
+
|| result.data.some(f => !f || f.scan_id !== scan.id || typeof f.id !== 'string' || !f.id || seen.has(f.id) || !['critical', 'high', 'medium', 'low', 'info'].includes(String(f.severity).toLowerCase()) || f.status !== 'open');
|
|
71
|
+
if (invalid || (Array.isArray(result.data) && new Set(result.data.map(f => f.id)).size !== result.data.length)) {
|
|
72
|
+
output(JSON.stringify({ verdict: 'inconclusive', reason: 'invalid_findings_pagination', runId: value, scanId: scan.id }));
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
total = result.total;
|
|
76
|
+
result.data.forEach(f => seen.add(f.id));
|
|
77
|
+
findings.push(...result.data); offset += result.data.length;
|
|
78
|
+
if (offset === total) break;
|
|
79
|
+
}
|
|
80
|
+
const verdict = savedRunGate(scan, findings);
|
|
81
|
+
output(JSON.stringify({ ...verdict, runId: value, scanId: scan.id, scope: 'saved_platform_not_pr_commit' }));
|
|
82
|
+
return verdict.exitCode;
|
|
83
|
+
}
|
|
84
|
+
let offset = 0;
|
|
85
|
+
do {
|
|
86
|
+
const result = await call(`${command === 'report' ? 'scans' : 'findings'}?limit=100&offset=${offset}`);
|
|
87
|
+
output(JSON.stringify(result));
|
|
88
|
+
offset += result.data?.length ?? 0;
|
|
89
|
+
if (!result.data?.length || offset >= result.total) break;
|
|
90
|
+
} while (true);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
95
|
+
run(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => { console.error(error.message); process.exitCode = 1; });
|
|
96
|
+
}
|
package/bin/hook.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Claude Code PreToolUse hook: `hackaiz hook claude`. Reads the hook JSON on stdin.
|
|
2
|
+
// Only Skill/MCP/package/workflow writes and install commands are checked (each check uses credits).
|
|
3
|
+
// Exit 2 blocks Danger, incomplete or unavailable checks. HACKAIZ_HOOK_FAIL_OPEN=1 explicitly opts into advisory mode.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
const COMPONENT = /(^|\/)(SKILL\.md|mcp\.json|\.mcp\.json|claude_desktop_config\.json|package\.json|requirements\.txt|Dockerfile)$|\.github\/workflows\/[^/]+\.ya?ml$/i;
|
|
6
|
+
const INSTALL = /\b(claude\s+mcp\s+add|npx\s+-y|npm\s+(i|install|add)\b|pnpm\s+add|yarn\s+add|bun\s+add|pip3?\s+install|uvx|pipx\s+install)\b|\b(curl|wget)\b[^|]*\|\s*(ba|z)?sh\b/i;
|
|
7
|
+
|
|
8
|
+
export function hookTarget(event) {
|
|
9
|
+
const tool = event?.tool_name; const input = event?.tool_input ?? {};
|
|
10
|
+
if (['Write', 'Edit', 'MultiEdit'].includes(tool) && typeof input.file_path === 'string' && COMPONENT.test(input.file_path.replace(/\\/g, '/'))) {
|
|
11
|
+
const content = tool === 'Write' ? input.content : tool === 'Edit' ? input.new_string : (input.edits ?? []).map((e) => e.new_string).join('\n');
|
|
12
|
+
if (typeof content === 'string' && content) return { name: input.file_path.split(/[\\/]/).slice(-2).join('/'), files: [{ path: input.file_path.split(/[\\/]/).pop(), content }] };
|
|
13
|
+
}
|
|
14
|
+
if (tool === 'Bash' && typeof input.command === 'string' && INSTALL.test(input.command)) return { name: 'install-command', files: [{ path: 'install.sh', content: input.command }] };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function hookCommand({ env, fetcher, stdin, errorOut }) {
|
|
19
|
+
const fr = env.HACKAIZ_LANG === 'fr';
|
|
20
|
+
const unavailable = (detail = '') => {
|
|
21
|
+
errorOut(fr ? `Vérification Hackaiz indisponible${detail} ; action non vérifiée.` : `Hackaiz check unavailable${detail}; action not checked.`);
|
|
22
|
+
return env.HACKAIZ_HOOK_FAIL_OPEN === '1' ? 0 : 2;
|
|
23
|
+
};
|
|
24
|
+
let event; try { event = JSON.parse(stdin); } catch { return unavailable(); }
|
|
25
|
+
const target = hookTarget(event);
|
|
26
|
+
if (!target) return 0;
|
|
27
|
+
if (!/^hkz_[A-Za-z0-9_-]{40,}$/.test(env.HACKAIZ_API_KEY ?? '')) return unavailable();
|
|
28
|
+
try {
|
|
29
|
+
const origin = new URL(env.HACKAIZ_URL ?? 'https://hackaiz.com');
|
|
30
|
+
if (origin.protocol !== 'https:' || origin.username || origin.password || origin.pathname !== '/' || origin.search || origin.hash) return unavailable();
|
|
31
|
+
// Opting into this hook authorizes paid checks, not automatic installation on a missing verdict.
|
|
32
|
+
const r = await fetcher(`${origin.origin}/api/public/v1/component-checks`, {
|
|
33
|
+
method: 'POST', redirect: 'error', headers: { authorization: `Bearer ${env.HACKAIZ_API_KEY}`, 'content-type': 'application/json' },
|
|
34
|
+
signal: AbortSignal.timeout(30_000),
|
|
35
|
+
body: JSON.stringify({ request_id: randomUUID(), channel: 'hook', name: target.name.slice(0, 120), language: env.HACKAIZ_LANG === 'fr' ? 'fr' : 'en', files: target.files }),
|
|
36
|
+
});
|
|
37
|
+
const data = await r.json();
|
|
38
|
+
if (!r.ok) return unavailable(` (${r.status})`);
|
|
39
|
+
if (data.verdict === 'danger') {
|
|
40
|
+
errorOut(`${fr ? 'Hackaiz bloque cette action' : 'Hackaiz blocked this'}: ${data.headline}. ${data.summary} ${(data.findings ?? []).slice(0, 3).map((f) => `[${f.rule}] ${f.title} (${f.file}:${f.line})`).join(' ')}`);
|
|
41
|
+
return 2;
|
|
42
|
+
}
|
|
43
|
+
if (data.complete !== true || !['safe', 'attention'].includes(data.verdict)) return unavailable();
|
|
44
|
+
if (data.verdict === 'attention') errorOut(`${data.headline}. ${data.summary}`);
|
|
45
|
+
return 0;
|
|
46
|
+
} catch { return unavailable(); }
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hackaiz/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@hackaiz/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Hackaiz command-line client: run passive security analyses and check components before you install them",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hackaiz": "bin/hackaiz.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.check.mjs"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"homepage": "https://hackaiz.com/tools",
|
|
22
|
+
"keywords": [
|
|
23
|
+
"security",
|
|
24
|
+
"cli",
|
|
25
|
+
"sast",
|
|
26
|
+
"mcp",
|
|
27
|
+
"skills",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"supply-chain",
|
|
30
|
+
"hackaiz"
|
|
31
|
+
]
|
|
32
|
+
}
|