Commit Graph
806 Commits
Author SHA1 Message Date
BaaRai 939f8d4976 db: drop URL userinfo in normalizeURL before storing assets.url
UpsertHTTPService and UpsertEndpoint both key assets.url on normalizeURL,
which lowercased scheme and host but kept u.User. A URL passed with
user:pass@ (insert_assets, server imports, sync) was stored verbatim and
the same URL with different credentials produced separate rows.

Set u.User = nil before serialising. Scheme, host, port, path and query
are unchanged; an @ in the path or query is preserved.

Tests: normalizeURL unit cases, plus PG tests for both upserts covering
credential stripping, dedup across different credentials, @ in path,
and an unchanged URL without userinfo.
2026-11-14 11:56:43 +01:00
BaaRai b5c0bb9f33 Merge pull request #222 from BaaRai/fix/scan-ingest-strip-userinfo
fix(agent): strip URL userinfo in scan and bbot ingestion
2026-11-14 01:12:17 +01:00
BaaRai 9d75df335f fix(agent): strip URL userinfo in scan and bbot ingestion
scanURL kept the userinfo of httpx, ffuf and nuclei URLs, so a line such
as http://user:pass@host/path stored the credentials in assets.url, in
the notable items and in the facts. scanURL now drops u.User before
re-serializing; scheme, host, port, path and query are kept.

The nuclei fact detail also embedded the raw host and matched-at values,
the httpx detail the raw host and the ffuf detail the raw redirect
location. They now go through stripUserinfo, which removes only the
userinfo of a URL or bare host:port and keeps every other byte, so a
value without userinfo is unchanged and fact deduplication is the same.

bbotTyped drops the userinfo of the typed url of an in-scope URL event
before it becomes an asset.

Rows already stored with userinfo are not rewritten here.
2026-11-14 01:02:16 +01:00
BaaRai fe9814c5f6 Merge pull request #221 from BaaRai/chore/db-test-cleanup-order
test(db): close the pool after the row cleanups, report cleanup errors
2026-11-14 00:51:55 +01:00
BaaRai 53fc8478da test(db): close the pool after the row cleanups, report cleanup errors
A `defer d.Close()` runs when the test function returns, before its
t.Cleanup functions. The cleanups that delete the test's rows then ran on
a closed pool, the error was ignored, and the rows stayed in the database
for the next run. PR #220 fixed three files; this applies the same pattern
to every remaining db test that combines `defer d.Close()` with row
cleanups.

- Close the pool with t.Cleanup, registered before the row cleanups so it
  runs last (LIFO).
- Report cleanup errors with t.Errorf through small helpers in
  db/cleanup_helpers_test.go. A profile the test already deleted is not an
  error.

Test files only. No assertion or production change.
2026-11-14 00:38:00 +01:00
BaaRai edc2f4da6d Merge pull request #220 from BaaRai/fix/db-test-isolation
test(db): make profile and company-scope tests repeatable on one database
2026-11-14 00:19:41 +01:00
BaaRai 01a9dae49e test(db): make profile and company-scope tests repeatable on one database
TestPoolProfilesOrder, TestProfileRetryRoundTrip and
TestProfileRetryClampedOnSave closed the pool with defer d.Close(), which
runs before t.Cleanup, so the row-deleting cleanup hit a closed pool and its
error was discarded. The fixed profile names then collided with the
llm_profiles name unique key on the next run. Close via t.Cleanup registered
first, give the profiles per-run names, and report cleanup errors.
TestLLMRetryPolicyRoundTrip had the same ordering and now restores the
setting before the pool closes.

TestCompanyScopeAttribution derived its names from MAX(assets.id) and never
removed its company, so the next pass found the old company holding the same
fixed 198.51.100.0/24 scope and the in-CIDR IP was attributed to it. Use a
time plus random suffix and a per-run /24 from 198.18.0.0/15, delete the
company (its scope cascades) after the assets, and report cleanup errors.
2026-11-13 23:46:42 +01:00
BaaRai 4e126a8524 Merge pull request #219 from BaaRai/feat/release-provenance
release: provenance attestation, CycloneDX SBOM and dry-run trigger
2026-11-13 23:28:15 +01:00
BaaRai abb6cb177d release: add build provenance attestation, CycloneDX SBOM and a dry-run trigger
- release job: attest dist/*.zip with actions/attest-build-provenance
  (id-token: write, attestations: write; contents stays write for the Release).
- SBOM: syft scans the bin-linux-amd64 binary (embedded Go modules), plus
  web/package-lock.json and go.mod/go.sum; artex-<tag>.cdx.json is checked for
  pkg:golang and pkg:npm entries and attached next to SHA256SUMS.
- workflow_dispatch dry run: builds, SBOM, attestations and workflow artifacts;
  the Release step and the docker job only run on push.
- README: Verifying a release section.
2026-11-13 23:17:19 +01:00
BaaRai 5122baa77f Merge pull request #218 from BaaRai/feat/secrets-phase3b-evidence
secrets: encrypt the evidence snapshot heads at rest (phase 3b)
2026-11-13 23:11:53 +01:00
BaaRai fcee6af45e secrets: encrypt the evidence snapshot heads at rest
traffic_evidence_snapshots.req_head and resp_head join the secret column registry (AAD per column, enc:v1:). Heads are sealed at insert and opened on every read; the startup key check, the no-key refusal and the idempotent backfill cover them. Archives keep the heads sealed; the import opens them to check the snapshot ID and the insert seals them once.
2026-11-13 22:56:52 +01:00
BaaRai b74ff21e11 Merge pull request #217 from BaaRai/feat/secrets-phase3-tools-exec
secrets: encrypt the exec spec of custom tools at rest
2026-11-13 22:07:01 +01:00
BaaRai 7899662aee secrets: keep a masked tool secret only for its saved destination
As for the MCP env (#213), a placeholder in a custom tool's exec keeps the
stored header, proxy or body only while the url (exact string) and the kind
are unchanged. A changed or emptied url, or a kind change, with a resolved
placeholder is refused (400 on the API and the test endpoint, a tool error
for update_custom_tool) before any write and before the test request is
sent. An explicit value with a new url is allowed.
2026-11-13 21:58:19 +01:00
BaaRai 68976a7680 secrets: encrypt the exec spec of custom tools at rest
Phase 3 of secrets at rest (#213, #216), same AES-256-GCM scheme and key.

- tools.exec joins the secret columns for system=false rows (AAD
  tools.exec): the whole JSON is sealed in the {"__artex_enc":"enc:v1:..."}
  envelope, so headers, body, proxy, script code and command are covered;
  {} stays {}
- CreateCustomTool and UpdateCustomTool seal it; scanTool opens it, so
  GetTool, ListTools and ListCustomTools (and through them customTools,
  the platform tools and the test endpoint) see the clear JSON in memory
- the startup backfill, the no-key refusal and the wrong-key check cover
  it, for system=false rows only; built-in rows stay in clear for the seeds

API: GET /api/tools returns the header values, proxy and body of a custom
tool masked, with headers_set, proxy_set and body_set. On update (API and
update_custom_tool) the placeholder keeps the stored value, an explicit
value replaces it and "" clears it; on create (API and create_custom_tool)
a placeholder is refused. The test endpoint takes an optional key: the
saved tool's stored values replace the placeholders; the editor sends it.
2026-11-13 21:52:27 +01:00
BaaRai 5aab3d3df8 Merge pull request #216 from BaaRai/feat/secrets-phase2
secrets: encrypt search keys, proxies and the LLM profile proxy at rest
2026-11-13 21:40:04 +01:00
BaaRai dc169b074f secrets: encrypt search keys, proxies and the LLM profile proxy at rest
Phase 2 of secrets at rest (#213), same AES-256-GCM scheme and key.

- llm_profiles.proxy joins the secret columns (AAD llm_profiles.proxy);
  scanProfile decrypts it for both column lists, SaveProfile encrypts
  it, an empty proxy stays NULL
- brave_search_api_key, tavily_search_api_key, web_search_proxy and
  global_proxy are stored encrypted (AAD settings.<key>) through
  GetSetting, SetSetting and InsertSettingIfAbsent; other settings stay
  in clear
- the startup backfill, the no-key refusal and the wrong-key check
  cover the new values; legacy plaintext is read as-is until backfill

API: the proxies are returned masked as a whole (placeholder plus a
*_set flag) by GET /api/settings, GET /api/llm/profiles and GET /api/llm.
On write the placeholder keeps the stored value, an explicit value
replaces it and "" clears it; the placeholder is never stored. The test
and model-list endpoints resolve the placeholder to the stored proxy.
2026-11-13 21:33:27 +01:00
BaaRai 37e5b69877 Merge pull request #215 from BaaRai/feat/scan-ingest-jsonl
agent: ingest nuclei, httpx and ffuf output in ingest_scan_output
2026-11-13 21:09:48 +01:00
BaaRai 07fe075c12 agent: ingest nuclei, httpx and ffuf output in ingest_scan_output
Extends ingest_scan_output with three formats:

- nuclei-jsonl: one tool_output fact per result and an asset for the
  scanned target when it is valid; no automatic finding
- httpx-jsonl: one HTTP service asset and one fact per URL
- ffuf-json: one URL asset and one fact per result, read token by token
  with encoding/json; a file cut short keeps the complete results

JSONL lines go through readJSONLines, taken from the bbot ingester
(64 KiB per line; longer lines and invalid JSON are skipped and
counted). bbot now uses the same reader, with no behaviour change.
One file yields at most 50000 assets and facts; records past the cap
are counted in over_cap and the result is marked truncated.
2026-11-13 19:15:59 +01:00
BaaRai a9b57d0627 Merge pull request #214 from BaaRai/feat/scan-ingest-core
Agent: ingest_scan_output for nmap XML files (worker, additive)
2026-11-13 18:56:59 +01:00
BaaRai 374c985d06 agent: add ingest_scan_output for nmap XML files
A worker can write nmap output with -oX and call ingest_scan_output with
the format and path. The file is parsed deterministically: assets go
through the insert_assets handler, each host becomes one tool_output
fact listing its open ports, and the model gets only counters and at
most 20 notable items per type.

- path is resolved under the run working directory; "..", an absolute
  path outside it and a symlink leading outside are refused; 256 MiB cap
- the XML is streamed host by host with encoding/xml; a file cut short
  keeps the complete hosts and returns truncated: true
- target text (banners, products, hostnames) is kept only as a
  Go-quoted string in the fact detail
- re-ingesting a file upserts the same assets and skips identical facts

Worker-only, like ingest_bbot. nuclei, httpx and ffuf follow separately.
2026-11-13 16:22:37 +01:00
BaaRai adf4b6d911 Merge pull request #213 from BaaRai/feat/secrets-at-rest
Encrypt stored secrets at rest in PostgreSQL
2026-11-13 15:39:44 +01:00
BaaRai 4f785a8b4f Guard masked MCP env against a destination change
A masked env value keeps the stored secret only while transport, command, args (canonical JSON) and url are unchanged, as notify.PrepareConfigUpdate does for channels. A changed destination with a masked value is refused (400). The credentials tab drops reveal/copy and shows a stored password as "stored, hidden".
2026-11-13 15:02:47 +01:00
BaaRai 07cc87c1a3 Encrypt stored secrets at rest in PostgreSQL
Seal the four credential columns with AES-256-GCM (random nonce per value,
AAD "<table>.<column>"): llm_profiles.api_key and task_credentials.password
store "enc:v1:<base64(nonce||ciphertext)>"; mcp_servers.env and
notification_channels.config store {"__artex_enc":"enc:v1:..."} over the
canonical JSON. The db layer encrypts on write and decrypts on read, so agent
tools (LLM provider, MCP, notifier, test credentials) get plaintext
server-side. Decryption failure is an error, never a plaintext fallback.

Key: ARTEX_SECRET_KEY (32 bytes, base64 or hex) or keys/secret.key next to
jwt.key, generated with crypto/rand at first start (0600). Startup refuses
when encrypted values exist and no key is available, and when the key does
not decrypt them. An idempotent backfill under the schema advisory lock
encrypts legacy plaintext rows and logs only a count.

API: MCP env is masked on read and masked placeholders on write keep the
stored value; task credential passwords are masked server-side and a masked
password on write is refused. ARTEX_SECRET_KEY is scrubbed from the process
environment with the other startup secrets.
2026-11-13 14:42:56 +01:00
BaaRai 6620b5e196 Merge pull request #212 from BaaRai/feat/noise-prompt
Agent: low-and-slow scanner guidance under a noise profile (off by default)
2026-11-13 14:01:13 +01:00
BaaRai 0116786f02 Fix review: dalfox --rate-limit/--workers, drop passive subfinder -t
Dalfox v3.2.4 (Rust) has no -w and exposes a global --rate-limit plus --workers (docs/content/reference/cli.md). Subfinder -t only bounds concurrent DNS lookups in active mode, so the passive line is removed; the generic fallback covers it.
2026-11-13 12:17:51 +01:00
BaaRai 4595274fd6 Agent: low-and-slow scanner guidance under a noise profile (off by default)
When ARTEX_NOISE_PROFILE is set, the planner, worker, main agent and chat system prompts gain a code-owned block with the profile per-host limits and exact rate/delay flags for the scanners installed in the image. With no profile the block is empty, so every prompt is byte-for-byte unchanged.

README documents the block.
2026-11-13 12:05:14 +01:00
BaaRai 0edf3f0fa7 Merge pull request #211 from BaaRai/feat/always-allow-argv
Intercept: always allow a pending Bash prefix (argv rule, code-owned denylist)
2026-11-13 11:36:25 +01:00
BaaRai 62e65f1235 Intercept: always allow a pending Bash prefix (argv allow rule, code-owned denylist)
POST /api/intercept/pending/{id}/always-allow derives an argv allow rule from the stored pending row (never from the body), at priority -1000, reusing an identical enabled rule, and leaves the request pending. A code-owned denylist of interpreters, launchers, package managers, orchestration and destructive programs refuses such rules at creation, in the manual rule form, and neutralises argv allow rules at matching; argv deny and ask rules still match.
2026-11-13 11:24:25 +01:00
BaaRai e2fd4908f7 Merge pull request #210 from BaaRai/feat/argv-prefix-rules
Intercept: argv prefix rules (match_type=argv)
2026-11-13 10:33:04 +01:00
BaaRai 2457a1f103 Intercept: argv prefix rules (match_type=argv) 2026-11-13 10:14:39 +01:00
BaaRai c5116f27ad Merge pull request #208 from BaaRai/feat/closed-question-judge
Intercept judge: closed-question mode (JEV), fail-closed to ask
2026-11-13 09:26:36 +01:00
BaaRai 06808396cc Intercept judge: review fixes for the closed-question mode
- A destructive or operator_policy unknown asks, even when read_only is yes.
- An enabled closed judge with no completer wired asks (judge_not_wired)
  instead of falling through to allow.
- The argument cap is checked before the review input is built.
- MaxTokens for the closed reply raised to 1536.
- The operator_policy question also covers a whitelist policy that fails to
  authorize the action; golden digest updated.
2026-11-13 08:40:02 +01:00
BaaRai 9a927846ef Intercept judge: closed-question mode, fail-closed to ask
The LLM fallback judge gets a closed mode (default when the judge is on):
the model answers eight code-owned questions with yes|no|unknown and one
evidence line each, in one call; the decision is taken in code. Every
failure (invalid output, missing key, value outside the vocabulary, a
failed call, arguments above the 256 KiB cap) is ask. The custom judge
prompt becomes bounded operator-policy data (8000 runes). The answers are
stored in the intercept audit and shown in the approval detail.

freeform keeps the previous behaviour; defaultJudgeFailAction changes
from allow to ask.
2026-11-12 22:35:11 +01:00
BaaRai 558b685886 Merge pull request #207 from BaaRai/feat/coverage-ledger
Coverage ledger: record examined, ruled-out and untested risk classes
2026-11-12 22:00:29 +01:00
BaaRai 101c92d516 Coverage ledger: review fixes (one-line capped fields, framed gap lines, per-run end_reason)
Normalise and length-check ledger fields in db.RecordCoverage, frame the planner gap lines in the data envelope, cap the wrap-up (25) and report (200) gap lists, cap current rows at 2000 per task. Replace the activity budget marker with tasks.end_reason, written with the guarded terminal status and cleared on revive. Generic 500 message in taskCoverage; early return on an empty ledger in the planner block.
2026-11-12 20:48:34 +01:00
BaaRai 6f3a59e205 Coverage ledger: record examined, ruled-out and untested risk classes
Workers record per (surface, risk_area) outcomes with the new record_coverage
tool (reported, no_issue_found, ruled_out, not_applicable, needs_follow_up).
Gaps are needs_follow_up entries, entries missing their required evidence or
reason, and goal vulnclass values with no entry (unrecorded_risk_class), so an
absent finding never reads as a clean target. Completeness marks timeout,
budget wrap-up, pause, abort and running tasks as incomplete with a caveat.

Surfaced in the planner context (one line per round, full gap list on the
timeout/budget wrap-up round), GET /api/tasks/{id}/coverage, the coverage card
and the Markdown report. Follows coverage_enabled; no new env var.
2026-11-12 20:16:23 +01:00
BaaRai afa6f4faab Merge pull request #206 from BaaRai/feat/hierarchical-budgets
Hierarchical budgets: min(child, parent) with forced wrap-up (off by default)
2026-11-12 19:53:52 +01:00
BaaRai d234e341fc Budget docs: wrap-up is typical, not deterministic; [budget] logged per planner round only 2026-11-12 19:18:07 +01:00
BaaRai 4cde1a889f Budgets: revive clears the settle state, document config, allow update_finding_report in wrap-up
- resetSettledRun clears settling and the budget ledger; a done/failed task
  revived to running now plans and claims again (was gated by the old wrap-up).
  resetTimeoutRevival calls it. Test: TestResetSettledRunRestartsBudget.
- docker-compose.yml passes the four ARTEX_*_BUDGET_* variables through.
- .env.example and README document the variables, the wrap-up, the in-memory
  ledger and the practical floor.
- llmrec: the usage sink runs even without a PostgreSQL recorder; comment fixed.
- BudgetWrapupBlocks allows update_finding_report.
2026-11-12 18:48:41 +01:00
BaaRai 16ddfc7779 Hierarchical run budgets: min(child, parent) with forced wrap-up (off by default)
Per-level token/turn budgets: task -> intent (worker run) -> planner round
(counted inside the task). A child's effective budget is
min(own budget, parent's remaining above the wrap-up reserve). When the task
can no longer afford one worker, workers stop claiming intents and the task
settles through one forced wrap-up planner round (the existing settle
sequence, budget words instead of the timeout words).

Config: ARTEX_TASK_BUDGET_TOKENS, ARTEX_TASK_BUDGET_TURNS,
ARTEX_INTENT_BUDGET_TOKENS, ARTEX_INTENT_BUDGET_TURNS. 0 or unset = unlimited;
with all four unset nothing changes.
2026-11-12 17:27:28 +01:00
BaaRai 4f2c6ef789 Merge pull request #205 from BaaRai/feat/egress-governor
Egress governor: noise profiles (off by default), per-host pacing, dedupe and target UA
2026-11-12 17:00:40 +01:00
BaaRai d116517c64 Egress: return large unshared responses without an error, namespace the transport cache per client, pace WebFetch only 2026-11-12 16:44:08 +01:00
BaaRai 6a5adf73fe Docs: WebFetch results are coalesced, not cached 2026-11-12 16:33:22 +01:00
BaaRai 3b5b2c3b88 WebFetch: coalesce only, never cache; pin the gate-off redirect test to the off profile 2026-11-12 16:33:14 +01:00
BaaRai 0b59f4670b Egress governor: noise profiles (off by default), per-host rate limit, dedupe and short cache
ARTEX_NOISE_PROFILE = off | quiet | balanced | aggressive drives one shared
governor used by WebFetch, the http custom tools and the enrich probe: token
bucket, bounded jitter and concurrency per host, a global cap, backoff on
429/503 honouring Retry-After with bounded retries, per-task coalescing and
a short cache of identical GETs (never POST/PUT/PATCH/DELETE), and a
per-host noise log line each minute. Off (the default) is a strict
passthrough.

Removes the fixed artex-enrich/1.0 User-Agent; ARTEX_TARGET_USER_AGENT sets
one under a profile (empty = unchanged).
2026-11-12 16:30:56 +01:00
BaaRai b789dd2eae Merge pull request #204 from BaaRai/feat/tool-rag-lexical
Tool RAG: lexical projection of the bound tool catalog (off by default)
2026-11-12 16:16:22 +01:00
BaaRai e640c1b0fd Tool RAG: pin the keep set (harness, Skill, deferred) in a test 2026-11-12 15:58:34 +01:00
BaaRai f3a20ef2c6 Tool RAG: lexical projection of the bound tool catalog (off by default)
When ARTEX_TOOL_RAG is enabled, AugmentTools keeps the core tools plus the
top-k (ARTEX_TOOL_RAG_TOPK, default 12) BM25-ranked bound tools for the
worker intent / planner goal. Runs after ToolResolve, so it only removes;
falls back to the full list on empty query, catalog under 20, or scorer error.
2026-11-12 15:42:31 +01:00
BaaRai 34ece5937a Merge pull request #203 from BaaRai/feat/untrusted-boundary-default-on
Untrusted boundary ON by default (opt-out via ARTEX_UNTRUSTED_BOUNDARY=0)
2026-11-12 14:53:27 +01:00
BaaRai 8c3bae41f6 Untrusted boundary ON by default (opt-out via ARTEX_UNTRUSTED_BOUNDARY=0)
ARTEX_UNTRUSTED_BOUNDARY unset or empty now enables the boundary.
0, false, off and no (case-insensitive, trimmed) disable it; any other
value keeps it on. Docs state the default, the opt-out and the measured
cost (about 154 bytes per tool result plus one system note).
2026-11-12 14:46:44 +01:00