feat(guard): egress catch/log/block layer installed at CLI entrypoint; audit Part 6-7
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { appendFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
/**
|
||||
* Neuron Egress Guard
|
||||
*
|
||||
* Catches and records every outbound HTTP(S) request made through
|
||||
* globalThis.fetch, and optionally blocks vendor endpoints.
|
||||
*
|
||||
* Modes (NEURON_GUARD):
|
||||
* - unset or "log" : record all egress, block nothing
|
||||
* - "strict" : additionally BLOCK denylisted vendor hosts
|
||||
*
|
||||
* Log: ~/.local/state/neuron-guard.jsonl (one JSON object per request)
|
||||
* Overrides: NEURON_GUARD_ALLOW="host1,host2" exempts hosts from blocking.
|
||||
*
|
||||
* Installed at the very top of the CLI entrypoint so no provider, plugin,
|
||||
* or update path can make an unobserved request.
|
||||
*/
|
||||
|
||||
type Entry = { t: string; host: string; url: string; blocked?: boolean }
|
||||
|
||||
const DENYLIST = ["opencode.ai", "anomalyco.com", "github.com/anomalyco"]
|
||||
|
||||
const LOG_PATH = path.join(
|
||||
process.env.XDG_STATE_HOME || path.join(homedir(), ".local", "state"),
|
||||
"neuron-guard.jsonl",
|
||||
)
|
||||
|
||||
import { appendFileSync, mkdirSync } from "node:fs"
|
||||
|
||||
function log(entry: Entry) {
|
||||
try {
|
||||
mkdirSync(path.dirname(LOG_PATH), { recursive: true })
|
||||
appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n")
|
||||
} catch {
|
||||
// never break the app over guard logging
|
||||
}
|
||||
}
|
||||
|
||||
function hostOf(url: string): string {
|
||||
try {
|
||||
return new URL(url).host
|
||||
} catch {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export function installGuard() {
|
||||
const mode = process.env.NEURON_GUARD ?? "log"
|
||||
const strict = mode === "strict"
|
||||
const allow = (process.env.NEURON_GUARD_ALLOW ?? "")
|
||||
.split(",")
|
||||
.map((h) => h.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const original = globalThis.fetch
|
||||
const guarded = async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url =
|
||||
typeof input === "string"
|
||||
? input
|
||||
: input instanceof URL
|
||||
? input.href
|
||||
: (input && "url" in input && typeof input.url === "string" ? input.url : String(input))
|
||||
const host = hostOf(url)
|
||||
const entry: Entry = { t: new Date().toISOString(), host, url }
|
||||
if (strict && !allow.includes(host)) {
|
||||
for (const denied of DENYLIST) {
|
||||
if (host === denied || host.endsWith("." + denied) || url.includes(denied)) {
|
||||
entry.blocked = true
|
||||
log(entry)
|
||||
throw new Error(`Neuron guard: egress to ${denied} is blocked (NEURON_GUARD=strict)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
log(entry)
|
||||
return original(input, init)
|
||||
}
|
||||
// preserve fetch-adjacent properties attached by the runtime
|
||||
for (const key of Object.keys(original)) {
|
||||
try {
|
||||
;(guarded as unknown as Record<string, unknown>)[key] = (original as unknown as Record<string, unknown>)[key]
|
||||
} catch {}
|
||||
}
|
||||
globalThis.fetch = guarded as typeof globalThis.fetch
|
||||
}
|
||||
|
||||
export * as Guard from "."
|
||||
@@ -1,3 +1,6 @@
|
||||
import { installGuard } from "./guard"
|
||||
|
||||
installGuard()
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
|
||||
Reference in New Issue
Block a user