9 Commits
Author SHA1 Message Date
BaaRai 1f4cb65ca6 feat(intercept): target-gate schema, opt-in flag and approval requests (S3b 1.2-1.4)
Storage, flag and request path of the approval gate for asset targets. The
gate itself is not wired into the tool hook yet (steps 1.5 to 1.7): with this
change nothing is extracted, resolved or blocked, whatever the flag says.

Schema and store (1.2)
- tasks.target_gate, conversations.target_gate (default false).
- intercept_pending.target_key and a partial unique index: one open request
  per scope (task or conversation) and host:port.
- target_pins (no foreign keys: a pin outlives its task, its conversation and
  its request row) and target_gate_exemption_log. All DDL is idempotent; no
  CHECK constraints; the cascade on intercept_pending.conversation_id stays.
- db/target_pins.go: pin lookup with port precedence, list, revoke,
  find-or-create of a request, and ResolveTargetRequest, which settles the
  row, supersedes the old pin and writes the new version in one transaction.
- Deleting a task or an archive stub revokes its pins and settles its open
  target requests in the delete transaction. DeleteConversation and
  DeleteConversations revoke the pins of the removed conversations in the
  same transaction as the delete.

Opt-in flag (1.3)
- Per task through the create and metadata-patch paths, per conversation
  through create and patch; exposed in the task DTO. A retest conversation
  takes the flag of the finding's task, never its pins. A spawned task starts
  with the gate off.
- The task flag is atomic and read on every call through TargetScope.Enabled.
  TargetScope now carries the Interceptor that owns the gate.

Request, wait, decide (1.4)
- pendingManager keeps a list of waiters per request; resolve wakes all.
- RequestTargets: find-or-create per target, one card per request, wait on
  channel, poll, context and wait bound. A plain request is never settled by
  a call that leaves. A request merged with an ask is bound to its creating
  call and a timeout never allows it.
- Decide records the approver (subject of the session token) and the remote
  address for every human decision, classic asks included. On a target
  request "allowed" pins the displayed addresses; that is the only writer of
  pins.
- A request raised while the global egress proxy is set says the approval is
  name-only and its pin is stored with name_only_effective.
- Routes: GET/POST .../target-pins and .../target-pins/{pid}/revoke under
  /api/tasks/{id} and /api/conversations/{id}; decision_source=target filter.
- ValidateExemptionHost: an exemption written as an IP literal may be private
  unicast, never loopback, link-local or metadata, unspecified or multicast.
2026-11-10 23:05:47 +01:00
BaaRai d0d153b768 fix(auth): trusted proxies for the login rate limit, 12 characters in reset-password.sh
- ARTEX_TRUSTED_PROXIES (comma-separated CIDRs, empty by default). When
  the TCP peer is listed, the login limit reads X-Forwarded-For from the
  right and keys on the first hop that is not a trusted proxy; otherwise
  the header is ignored. Without it every client behind a reverse proxy
  shares one bucket and an attacker can block the admin. Passed through
  by docker-compose.yml and listed in .env.example.
- README: nginx example sends X-Forwarded-For; document the variable;
  the binary's default listen address is 127.0.0.1:8787.
- reset-password.sh refuses passwords under 12 characters, counted in
  characters like the server.
2026-11-05 16:57:48 +01:00
BaaRai 2419038884 fix(auth): rate-limit login per IP, 12-character passwords, loopback default
- POST /api/auth/login: 10 attempts per minute per TCP peer IP
  (X-Forwarded-For ignored), then 429 with Retry-After. The block starts
  at 1 minute and doubles on each attempt while blocked and on each new
  block, capped at 15 minutes. A successful login clears the IP. No
  per-account lockout. In-memory, idle entries swept after 15 minutes,
  at most 10,000 IPs.
- New passwords (init, change-password, /setup page) need 12 characters.
  Login does not check length, so existing 8-character passwords work.
- The binary's -addr default is 127.0.0.1:8787. The Docker image passes
  -addr :8787 explicitly and is unchanged.
- GET /api/health returns {"ok":true} without a valid token; with one it
  still returns service and version for the web UI top bar.
2026-11-05 16:48:27 +01:00
BaaRai 65bdc5d73c fix(auth): keep the JWT signing key in a dedicated volume
The server wrote jwt.key to the executable's directory, /app in the
image, so it lived in the container's writable layer and every container
rebuild generated a new key and signed all users out.

- ARTEX_JWT_KEY_DIR selects the key directory (default: config.BaseDir(),
  unchanged for non-Docker installs). docker-compose.yml sets it to
  /app/keys and bind-mounts ./keys there; the Dockerfile declares the
  volume. The key stays out of data/, which the file manager browses.
- loadOrCreateJWTKey creates the directory (0700). When keyDir/jwt.key is
  missing it migrates dataDir/jwt.key, then BaseDir()/jwt.key (mode 0600,
  old file removed), otherwise generates a new key. An existing key is
  never overwritten; a short one is now a startup error.
- .gitignore and .dockerignore exclude keys/.
- README and CHANGELOG updated; the 0.5.0 known issue is removed.
2026-11-04 09:18:11 +01:00
GRAB Racem 2aab3e8210 fix(auth): require a setup token for /api/auth/init and bind 8787 to localhost
POST /api/auth/init accepted any caller while auth.password_hash was empty,
so whoever reached port 8787 first could set the admin password and take the
instance. When no password is set at startup, the server now generates a
random one-time setup token and writes it to the server log. authInit
compares it in constant time, rejects a missing or wrong token with 403, and
clears it once the password is stored.

The setup page has a field for the token and api.initPassword sends it.
docker-compose publishes 8787 on 127.0.0.1 only, like 8788. README says where
to find the token.
2026-11-03 22:18:27 +01:00
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00
Autumn-27 66b7006c69 fix bug 2026-10-07 07:45:28 -04:00
Autumn-27 df50c281bc fix(security): jwt.key 移出可浏览工作区(存项目根,自动从 data 迁移)
工作空间文件管理器的根目录是 dataDir(s.m.dir),而 jwt.key 原来就存在
dataDir/jwt.key —— 会被列出/下载(签名密钥泄露→可伪造 token)/删除(重启重生
成→全体登出)。

- loadOrCreateJWTKey(keyDir, dataDir):密钥改存 keyDir/jwt.key;若旧 data/jwt.key
  仍在,启动时迁移(保留密钥不登出、删旧文件从工作区消失)
- New() 增加 keyDir 参数;生产传 config.BaseDir()(项目根),在 dataDir 之外,
  wsResolve 的路径限定使其不可列/读/下载/删
- 测试传临时目录,隔离且不触发迁移;.gitignore 忽略根目录 /jwt.key

注:项目根在 Docker 里不持久化,容器重建会重新生成密钥、需重新登录(单人系统可接受)。
2026-08-11 10:56:39 -04:00
Autumn-27andClaude Opus 4.8 0ef8561411 chore: ARTEX 初始化
ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-26 04:27:45 -04:00