feat(llm-security-copilot): port llm-security v5.1.0 to GitHub Copilot CLI
Full port of llm-security plugin for internal use on Windows with GitHub Copilot CLI. Protocol translation layer (copilot-hook-runner.mjs) normalizes Copilot camelCase I/O to Claude Code snake_case format — all original hook scripts run unmodified. - 8 hooks with protocol translation (stdin/stdout/exit code) - 18 SKILL.md skills (Agent Skills Open Standard) - 6 .agent.md agent definitions - 20 scanners + 14 scanner lib modules (unchanged) - 14 knowledge files (unchanged) - 39 test files including copilot-port-verify.mjs (17 tests) - Windows-ready: node:path, os.tmpdir(), process.execPath, no bash Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
901bf0ae12
commit
f418a8fe08
169 changed files with 37631 additions and 0 deletions
140
plugins/llm-security-copilot/hooks/scripts/update-check.mjs
Normal file
140
plugins/llm-security-copilot/hooks/scripts/update-check.mjs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env node
|
||||
// Hook: update-check.mjs
|
||||
// Event: UserPromptSubmit
|
||||
// Purpose: Check for newer plugin versions (max 1x/24h, cached).
|
||||
//
|
||||
// Protocol:
|
||||
// - Read JSON from stdin (consume, don't use)
|
||||
// - If newer version available: exit 0, stdout JSON { systemMessage: "..." }
|
||||
// - Otherwise: exit 0 silently
|
||||
// - Never block the user (always exit 0)
|
||||
//
|
||||
// Disable: LLM_SECURITY_UPDATE_CHECK=off
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
||||
import { resolve, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports for testing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
/**
|
||||
* Return true if `remote` is a newer semver than `local`.
|
||||
* Simple numeric comparison — no pre-release/build metadata.
|
||||
*/
|
||||
export function isNewer(remote, local) {
|
||||
const r = remote.split('.').map(Number);
|
||||
const l = local.split('.').map(Number);
|
||||
for (let i = 0; i < Math.max(r.length, l.length); i++) {
|
||||
const rv = r[i] ?? 0;
|
||||
const lv = l[i] ?? 0;
|
||||
if (rv > lv) return true;
|
||||
if (rv < lv) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main (only runs when executed directly, not when imported for tests)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const isDirectExecution = process.argv[1] &&
|
||||
resolve(process.argv[1]) === resolve(__dirname, 'update-check.mjs');
|
||||
|
||||
if (isDirectExecution) {
|
||||
main().catch(() => process.exit(0));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Opt-out
|
||||
if (process.env.LLM_SECURITY_UPDATE_CHECK === 'off') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Consume stdin (prevent pipe errors)
|
||||
try { readFileSync(0, 'utf8'); } catch { /* ignore */ }
|
||||
|
||||
// Resolve plugin root
|
||||
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || resolve(__dirname, '../..');
|
||||
|
||||
// Read installed version
|
||||
let installed;
|
||||
try {
|
||||
const pluginJson = JSON.parse(readFileSync(resolve(pluginRoot, '.claude-plugin/plugin.json'), 'utf8'));
|
||||
installed = pluginJson.version;
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Read repo URL
|
||||
let repoUrl;
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(resolve(pluginRoot, 'package.json'), 'utf8'));
|
||||
repoUrl = pkg.repository?.url;
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!installed || !repoUrl) process.exit(0);
|
||||
|
||||
// Cache
|
||||
const cacheDir = resolve(homedir(), '.cache/llm-security');
|
||||
const cachePath = resolve(cacheDir, 'update-check.json');
|
||||
|
||||
// Check cache
|
||||
try {
|
||||
if (existsSync(cachePath)) {
|
||||
const cache = JSON.parse(readFileSync(cachePath, 'utf8'));
|
||||
if (Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) {
|
||||
// Cache is fresh
|
||||
if (cache.latestVersion && isNewer(cache.latestVersion, installed)) {
|
||||
console.log(JSON.stringify({
|
||||
systemMessage: `🔄 llm-security v${installed} → v${cache.latestVersion} available. Update: ${repoUrl}`
|
||||
}));
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Corrupt cache — proceed to fetch
|
||||
}
|
||||
|
||||
// Fetch latest version from Forgejo raw API
|
||||
const fetchUrl = `${repoUrl}/raw/branch/main/.claude-plugin/plugin.json`;
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const res = await fetch(fetchUrl, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!res.ok) process.exit(0);
|
||||
|
||||
const remote = JSON.parse(await res.text());
|
||||
const latestVersion = remote.version;
|
||||
if (!latestVersion) process.exit(0);
|
||||
|
||||
// Write cache
|
||||
try {
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
writeFileSync(cachePath, JSON.stringify({ checkedAt: Date.now(), latestVersion }));
|
||||
} catch {
|
||||
// Cache write failure is non-fatal
|
||||
}
|
||||
|
||||
// Notify if newer
|
||||
if (isNewer(latestVersion, installed)) {
|
||||
console.log(JSON.stringify({
|
||||
systemMessage: `🔄 llm-security v${installed} → v${latestVersion} available. Update: ${repoUrl}`
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// Network error, timeout, parse error — silent exit
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue