Skip to content

refactor(workers): move security alert and attack-simulation handlers into apps/workers - #4408

Open
aflordelis wants to merge 7 commits into
mainfrom
refactor/move-security-workers
Open

aflordelis wants to merge 7 commits into
mainfrom
refactor/move-security-workers

Conversation

@aflordelis

Copy link
Copy Markdown
Contributor

Description

Refs #73

Moves the security alert processor and the Active Defense attack-simulation runner Lambdas from apps/client/server/security into apps/workers/src/security. Move-only, no behavior change. Refs, not Closes: this is one slice of #73, and cloudScan/wafScan stay in the client because admin API routes call them directly.

Customer note

none

Changes

  • git mv into apps/workers/src/security/: alertProcessor.ts, attackSimulation/runner.ts, attackSimulation/fingerprint.ts, and the 7 attackSimulation/probes/* files (commit 1 is 100% renames)
  • attackSimulation/types.ts stays in the client (it is the ingest route's payload contract); the runner and probes now import it through @server/*
  • Retargeted the handler paths in infra/securityAlerts.ts and infra/cron.ts; construct IDs are unchanged
  • Updated the probes path named in the SecOps triage prompt (secopsTriageService.ts)
  • Extended checkWorkersMovedDirs.test.ts so the moved files can't be recreated at the old path, and anchored both new handler strings in its infra scan
  • Listed src/security in apps/workers/README.md and CLAUDE.md

Guide for Testers

Needs a preview deploy (the change only touches where two Lambdas are bundled from).

  1. Open the preview app and sign in as an admin.
  2. Go to Sidebar → Admin → Security → Security Dashboard → Active Defense tab.
  3. Click Run Now.
    • Expected: no error toast; a new row appears in the runs table, and within a few minutes its Status reaches a finished state with probe counts filled in.

Regression Checks

  • Same dashboard, other tabs (Cloud, WAF) still load: they deploy from the client and are untouched.

What NOT to test

  • The security alert processor has no UI trigger; it is covered by the typecheck and bundle check below.

Additional Information

  • After this PR, cloudScan (infra/cron.ts) is the only Lambda handler still pointing into apps/client/server/security.
Verification and open items
Check Result
pnpm --filter @bike4mind/workers typecheck / client typecheck pass
pnpm --filter @bike4mind/workers test 85 files, 1234 passed, 1 skipped
client vitest run --project node 986 files, 12772 passed, 6 skipped
checkWorkersMovedDirs.test.ts pass; reverting a handler or recreating an old-path file fails it
pnpm lint:check pass
pnpm turbo:core:build && node scripts/check-lambda-bundle.mjs OK, 111 handlers bundled

Checklist

  • I have added the new AdminSettings to Staging, prod and the hydration script (if applicable)
  • I have made the UI/UX feel good (toasters, size, responsive, etc)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • If adding/modifying telemetry fields: updated telemetry data classification doc

… into apps/workers

Rename only; imports and infra handler paths are repointed in the next
commit.
…tries

The attack-simulation types stay in apps/client (the ingest route owns
the payload contract), so the runner and probes import them through
the @server bridge. The triage prompt now names the probes' new home.
@bike4mind-release-bot

bike4mind-release-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

🟠 Preview is 25 commits behind

Running 27ecc18 — test(scripts): anchor the attack-simulation runner handler in the infra…
PR head d250acc — fix: address review F1 - label the runner anchor with its own infra file
Gap 25 commits not in this preview
Deployed 2026-10-12 09:31 UTC (8 hours ago) by b4m-preview-dispatcher[bot]

⚠️ Testing this preview will not exercise the latest changes.
Refresh it with /deploy-preview 4408 in Slack.

Preview URL is in the internal deploys channel.

@bike4mind-release-bot

Copy link
Copy Markdown
Contributor

🚀 Preview is live at 27ecc18 — matches the PR head. Ready to test.

test(scripts): anchor the attack-simulation runner handler in the infra…

@bike4mind-release-bot bike4mind-release-bot Bot added the preview-deployed A preview stack is live for this PR (managed by the deploy bot; do not set manually) label Oct 12, 2026
@aflordelis
aflordelis marked this pull request as ready for review October 12, 2026 09:59
@aflordelis
aflordelis requested review from a team as code owners October 12, 2026 09:59
@aflordelis aflordelis added the bot-review Trigger Claude bot re-review (auto-removed after run) label Oct 12, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bot review. No blocking findings. Clean mechanical move, well-fenced.

What I checked

  • @server/* resolution. apps/workers/tsconfig.json maps @server/* to ../client/server/*, so @server/security/attackSimulation/types resolves to apps/client/server/security/attackSimulation/types.ts, which still exists. All seven probes and runner.ts were correctly repointed to this absolute alias; no ../types leftovers.
  • SST handler update. The admin "Run Now" endpoint invokes the Lambda by resource name (Resource.lambdaFunctionNames.attackSimulation at run-attack-simulation.ts#L45), not by handler path, so the retargeted handlers in infra/cron.ts#L451 and infra/securityAlerts.ts#L20 are the only wiring touched. Unchanged construct IDs mean SST should update in place; the preview deploy at head went green, consistent with that.
  • Guard coverage. checkWorkersMovedDirs.test.ts: MOVED empties the whole apps/client/server/security/attackSimulation/probes subtree; MOVED_MODULES blocks alertProcessor at security/ and fingerprint+runner at attackSimulation/ by file-stem, correctly leaving types.ts to stay. The infra-handler scan at L148-149 anchors both new handler strings, so a stale handler in securityAlerts.ts or cron.ts would fail the test. The sibling every apps/workers infra handler resolves to a real module file test also covers the two new handlers.
  • Stale references. No remaining imports of apps/client/server/security/{alertProcessor,attackSimulation} anywhere in the tree. The surviving references to apps/client/server/security/* are only to files the PR explicitly leaves behind (cloudScan, wafScan, wafHeaderRedaction, wafLogsInsights, resolveStage, etc.).
  • ASCII rule on added lines. The one em-dash replaced in secopsTriageService.ts:416 is on an added line and now uses a hyphen; the CLAUDE.md and workers-README additions are ASCII-only.

Notes

  • The runner and probes now reach their types through the @server/* bridge (absolute path) rather than a ../types relative path. The PR body calls this out as the deliberate tradeoff for leaving types.ts with the ingest route it is a payload contract for; agreed, no action.
  • The PR body's own "Unverified: that SST updates both functions in place rather than replacing them" was reduced-but-not-eliminated by the green preview deploy, since preview only exercises one stage. Worth watching the staging/prod deploy that follows merge for a replacement rather than an update.

Nothing requested from the author.

@github-actions github-actions Bot removed the bot-review Trigger Claude bot re-review (auto-removed after run) label Oct 12, 2026
@julsanchez julsanchez self-assigned this Oct 12, 2026
@julsanchez

julsanchez commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Test Report: PR #4408 refactor: move security alert and attack-simulation handlers into apps/workers

Overall result: ⚠️ PARTIAL (step 3 blocked by a preview environment gap; no bug found in this PR)

3 of 4 steps pass. Run Now invoked the relocated apps/workers/src/security/attackSimulation/runner.handler: it loaded, connected to the DB, created the run row and finished it in about 5s. The run then ended failed with 0 probes because the preview stage has no attack-simulation ingest token (SECOPS_ATTACK_SIMULATION_INGEST_TOKEN). That code path is untouched by this PR. To close this out, a maintainer needs to set that secret for stage pr4408, then QA re-runs step 3 (after the 30-min cooldown).

Accounts
  • qa-admin-e2e@test.com (admin) on pr4408; read-only sign-in on the pr4414 preview for an off-PR comparison (no runs started there, nothing changed).
  • Seeding: none.
Results by step

✅ Step 1 - Sign in as admin

OTC login landed on /new; sidebar and account loaded ("QA Admin"). No policy gate, no MFA.
Evidence: B4M-4408-00-login-admin.png

✅ Step 2 - Navigate to Active Defense

Account menu -> Admin -> Security -> Security Dashboard -> Active Defense. Tab loaded. History already held one manual run from 10/12 5:57 PM PST (not started by this QA run): failed, probes 0, errors 1.
Evidence: B4M-4408-02-active-defense-before-run.png

⚠️ Step 3 - Run Now (BLOCKED, environment)

POST /api/admin/security-dashboard/run-attack-simulation returned 202 with the inline alert "Attack simulation queued (run a518a085-d0ba-44c6-a038-5f23c9e2d004)". No error toast. After reload a new row appeared (8:25:49 PM PST, manual): failed, New/Persisting/Resolved 0, Probes 0, Errors 1, plus a 29-min cooldown banner. The API row has error: "Attack simulation ingest token is not configured." and finishedAt 5.6s after startedAt. The earlier 09:57 UTC run has the identical error. The expected "finished state with probe counts" was not reached.

Why this is an environment gap and not a PR bug:

  • In the moved runner.ts the only change is one import type path (99% similarity rename). The token read and link: [...allSecrets] in infra/cron.ts are unchanged.
  • The secret is declared at infra/secrets.ts:117 with default 'not-configured' and is set per stage; this PR does not touch secrets.ts. Any stage without a manually set token fails the same way.
  • The relocated Lambda clearly ran: the running row is written by the handler itself after connectDB and the safety/single-flight checks, and failRun then set it to failed.
  • Off-PR comparison: the pr4414 preview has no Active Defense runs (runs: []); I did not start a run on someone else's preview.

Evidence: B4M-4408-03-run-now-queued.png, B4M-4408-04-run-finished-failed.png

✅ Step 4 - Regression: Cloud and WAF tabs load

Cloud tab: "Cloud Security" with Infrastructure Baseline / Prowler Audit sub-tabs, empty state "No cloud security scan has been recorded yet for this stage", Run Cloud Scan button and weekly toggle. WAF tab: "No Firewall / WAF scan has been recorded yet for this stage", Run Firewall / WAF Scan, weekly toggle, Traffic overview "No WebACL attached for this stage". Both underlying GETs return 404 NotFoundError ("No ... snapshot found for this stage."), which the UI handles as its empty state. Scans were not started (out of scope).
Evidence: B4M-4408-05-cloud-tab.png, B4M-4408-06-waf-tab.png

Findings

Bugs in this PR (gating): None.

Pre-existing issues (do not gate):

  • P3: when the ingest token is missing, the runner executes all 6 probes and then calls failRun(runId, message), which does not save probesRun/probeErrors. The table shows "Probes 0" even though the probes ran, and the real cause (the error field) is not shown anywhere in the UI; users see only "failed / Errors 1". Pre-existing: the same logic is in the main-branch file and the only diff hunk in runner.ts is the import-type path. Evidence: B4M-4408-04-run-finished-failed.png.

Observations / open items (do not gate):

  • Blocked on a maintainer: SECOPS_ATTACK_SIMULATION_INGEST_TOKEN is not set for stage pr4408, so step 3's "finished with probe counts" outcome is unverified.
  • Intake showed one "preview gate" check as FAILURE alongside several preview gate SUCCESS entries; not investigated. The preview itself deployed and served fine.
  • The alert processor (SecurityAlertsSchedule, 5-min cron) was not verified: it has no UI trigger and QA has no AWS access.
  • The outer dashboard tab is labelled "Security Dashboard (Mock)" on an admin page with live data (confusing).
  • runner.ts header comment says "Executes 7 HTTP probes" but there are 6 (comment moved verbatim).
  • Cloud/WAF 404 empty-state responses are logged as console errors (Axios Error NotFoundError); noise only.
Cleanup
  • Environment left changed: one new manual Active Defense run row on pr4408 (run a518a085-d0ba-44c6-a038-5f23c9e2d004, failed), which starts a 30-min Run Now cooldown.
  • Cookie banner declined. No settings, users or MFA changed. Nothing changed on pr4414.
  • No code changes, commits, pushes, PR edits or reviews. Playwright browser closed.
Screenshot index
File Shows Image
B4M-4408-00-login-admin.png Signed in as QA Admin on /new B4M-4408-00-login-admin.png
B4M-4408-02-active-defense-before-run.png Active Defense tab before Run Now, with one prior failed run B4M-4408-02-active-defense-before-run.png
B4M-4408-03-run-now-queued.png "Attack simulation queued" alert after Run Now, no error toast B4M-4408-03-run-now-queued.png
B4M-4408-04-run-finished-failed.png New 8:25:49 PM row: failed, 0 probes, 1 error, cooldown banner B4M-4408-04-run-finished-failed.png
B4M-4408-05-cloud-tab.png Cloud tab empty state (regression check) B4M-4408-05-cloud-tab.png
B4M-4408-06-waf-tab.png Firewall / WAF tab empty state (regression check) B4M-4408-06-waf-tab.png

julsanchez
julsanchez previously approved these changes Oct 12, 2026

@julsanchez julsanchez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review pass 1 — approved

A clean move: the diff is renames plus import-path and handler-string edits, and the guard, bundle check and full typecheck all pass. Breaking things on purpose shows the guard and the bundle check both fail when they should.

Approving. I diffed the change at 27ecc18 with git diff -M. It is renames plus four kinds of edit: seven import type paths, two infra handler strings, one prompt path, and the guard and docs additions. No probe logic changed. assertTargetUrlIsSafe and the runner's domain guard are byte-identical. One P3 about a comment, below.

This review builds on the claude bot review and the QA pass at the same head, and does not redo them. QA's step-3 failed run comes from an environment gap: SECOPS_ATTACK_SIMULATION_INGEST_TOKEN is not set on pr4408. It does not gate this PR, and I agree. The relocated runner Lambda did load and write its run row. QA's failRun observation is pre-existing and is listed below.

Not blocking

  • F1 (P3) packages/scripts/src/checkWorkersMovedDirs.test.ts:147 — the infra/securityAlerts.ts comment also sits over the runner anchor, which lives in infra/cron.ts

    The added comment at L147 says infra/securityAlerts.ts, which no other moved handler lives in. It sits above two anchors: L148 alertProcessor.handler (which is in infra/securityAlerts.ts:20) and L149 attackSimulation/runner.handler (which is in infra/cron.ts:451). A reader would take the runner anchor to be a securityAlerts.ts handler. The comment pattern also says each anchor exists because its file is otherwise unscanned, which is not true of cron.ts: L134 already proves that file is read.

    Not wrong: the assertion itself is correct, and it fails when either handler is reverted. I reverted infra/securityAlerts.ts:20 to the old path and the test failed (1 failed | 47 passed).

    Fix. Put the runner anchor on its own line with // infra/cron.ts as its comment, or drop that anchor, since the L134 cron/ anchor already proves cron.ts is scanned. I would take the first: it keeps the explicit anchor the author added in the last commit.

Pre-existing, not introduced by this PR

  • From QA (carried forward, not my finding): when the ingest token is missing, failRun discards probesRun/probeErrors, and the UI never shows the error cause. The runner.ts hunk is the import line only, so this predates the PR.
  • runner.ts:4 says "Executes 7 HTTP probes" but there are 6 (QA observation; the comment moved unchanged).

What the PR claims to do

# Claim Holds? Evidence
C1 Move-only, no behavior change met git diff -M $(merge-base) 27ecc18: alertProcessor.ts, fingerprint.ts and probeUtils.ts are 100% renames. runner.ts (99%) and the 6 probes (95-98%) differ only in the import type ... types line.
C2 attackSimulation/types.ts stays in the client; the runner and probes import it through @server/* met apps/client/server/security/attackSimulation/types.ts still exists, and pages/api/admin/security-dashboard/attack-simulation-ingest.ts:17 imports it. It is type-only, so nothing from it is bundled at runtime.
C3 Handler paths retargeted; construct IDs unchanged met infra/cron.ts:450-451 (attackSimulationFunction) and infra/securityAlerts.ts:17-20 (SecurityAlertsSchedule): only handler changed. infra/web.ts:169 still links attackSimulationFunction.name.
C4 Probes path in the SecOps triage prompt updated met secopsTriageService.ts:416 now names apps/workers/src/security/attackSimulation/probes, which exists.
C5 The guard stops moved files from being recreated and anchors both handler strings met vitest run checkWorkersMovedDirs: 48/48. I reverted the securityAlerts.ts handler and the guard failed (no infra handler points into a moved directory).
C6 check-lambda-bundle.mjs: OK, 111 handlers bundled met Reproduced: OK: 111 handlers bundled, 10283 modules resolved. I broke a probe's ../fingerprint import and got Could not resolve "../nope", so the moved files really are inside the scan.
C7 Workers and client typecheck pass met Full pnpm turbo:typecheck: 41/41 tasks pass. CI Type Check is also green.
C8 Workers tests 1234 pass; client node project 12772 pass; lint pass untested Not re-run by me. At this head CI Run Tests (all shards), Run Tests (workers-integration) and Lint are green. No tests were added or moved.
C9 SST updates both functions in place (the PR's own checkbox marks this unverified) partial Reasoned, not run against AWS. On aws:lambda/Function, handler updates in place, and the logical names are unchanged. The preview deploy is green, and QA's Run Now ran the relocated runner. The alert processor's schedule is enabled only on production/dev (infra/securityAlerts.ts:30), so no preview has ever run it.
C10 After this PR, cloudScan is the only Lambda handler still pointing into apps/client/server/security met grep handler: 'apps/client/server/security' infra: the only hit is infra/cron.ts:308 cloudScan.handler.

Docs & changelog — complete

User-visible change: no — Internal relocation; nothing a user can see changes.

Expected Touched?
apps/workers/README.md yes
CLAUDE.md (apps/workers line) yes

Now-wrong: none. selfhost/, docs-site/ and docs/ have no references to the old paths. The only remaining reference into apps/client/server/security/attackSimulation is the ingest route's types import, which is intended.

Notes

Gates observed on 27ecc18, run by me in an isolated worktree. pnpm turbo:core:build OK. node scripts/check-lambda-bundle.mjs: 111 handlers bundled. packages/scripts vitest run checkWorkersMovedDirs: 48/48. pnpm turbo:typecheck: 41/41. CI at this head: every check is green and none failed. The preview gate FAILURE that Intake saw is now pass.

The bundle guard did not run in CI on this PR. lambda-bundle-guard.yml only triggers on package.json, the lockfile and apps/workers/tsconfig.json changes, so this PR's bundling evidence is the green preview deploy plus my local run above. That is not a defect in this PR. It means a move-only PR gets no CI bundle signal unless it also touches infra/ and so triggers a preview.

The first real run of alertProcessor from its new path will be on dev after merge. SecurityAlertsSchedule is enabled only for production/dev. Watch that stage's first 5-minute run, and check that the dev deploy updates the function rather than replacing it.

Ruled out. The moved files' remaining relative imports (./probes/*, ../fingerprint, ./probeUtils) all moved with them. The cross-tree imports (@server/utils/config, @server/security/resolveStage, @server/integrations/github/githubWorkflowTrigger, @server/utils/logger, @server/utils/eventBus) resolve through apps/workers/tsconfig.json's interim alias, the same way the other moved handlers do. git blame on both handler lines goes back only to the initial open-core import, so the old paths had no recorded reason to stay where they were. The PR merges cleanly onto origin/main (git merge-tree), and the head is 22 commits behind it.

Not reviewed. AWS-side state, which needs CloudWatch and deploy-plan access. The runner's success path ("finished with probe counts") is blocked by the missing preview secret, per QA.

Independently verified on 27ecc18: lambda bundle 111/111, checkWorkersMovedDirs 48/48 plus two deliberate-break checks, pnpm turbo:typecheck 41/41.


Reviewed origin/main…27ecc182 · 16 files, +33/-14 · 0 P0 · 0 P1 · 0 P2 · 1 P3

@julsanchez julsanchez added done reviewing Reviewed by the Manifold review board - do not queue again and removed awaiting review labels Oct 12, 2026
The runner handler lives in infra/cron.ts, not infra/securityAlerts.ts, so give its anchor its
own comment instead of sharing the securityAlerts.ts one.
@julsanchez

Copy link
Copy Markdown
Contributor

Review follow-up: F1 (P3) fixed in d250acc. The attackSimulation/runner.handler anchor in packages/scripts/src/checkWorkersMovedDirs.test.ts now has its own // infra/cron.ts comment instead of sitting under the infra/securityAlerts.ts one. vitest run checkWorkersMovedDirs still passes 48/48.

The branch also merges origin/main (merge commit, no force-push). It was 22 commits behind, and the merge was clean.

Not changed: the pre-existing runner.ts:4 "7 HTTP probes" comment. It was not a review finding, and this PR is move-only.

@julsanchez

Copy link
Copy Markdown
Contributor

QA delta since last run

@julsanchez your QA pass ran on 27ecc18. The new head is d250acccf.

Delta: 1 fix commit + a merge of main. No re-test needed.
Fix SHA What changed What to re-test Risk
d250acc F1: one comment line in the checkWorkersMovedDirs guard test Nothing; this is test-file comment only None
merge of origin/main Brings the branch up to date (22 commits, clean merge; no conflicts in this PR's files) Nothing PR-specific; the preview redeploys at the new head Low

The open item from the last pass is unchanged: step 3 (Run Now) still needs SECOPS_ATTACK_SIMULATION_INGEST_TOKEN set on the preview stage. Until it is, the run ends failed.

@julsanchez julsanchez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving at d250acc after the review follow-up push: F1 fixed (test comment only) plus a clean merge of main. Pass-1 review stands; see the follow-up comment above.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting ci issue fix done reviewing Reviewed by the Manifold review board - do not queue again merge conflict preview-deployed A preview stack is live for this PR (managed by the deploy bot; do not set manually) preview-stale qa_clarification qa: in_progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants