Commit graph

6 commits

Author SHA1 Message Date
e97e740c5a Gate web UIs behind auth and escape currency XSS
The po-web-ui and workorder-web-ui handlers had no auth: any
invocation path returned the full PO/WO DB. Add a fail-closed
shared-secret gate (X-Auth-Token / Bearer, constant-time compared to
WEB_UI_AUTH_TOKEN) so a future re-attached Function URL cannot
re-expose the data (URLs removed under INFRA-74). Wire the token from
the SSM String param /procurement-ingest/web-ui-auth-token.

Also fix stored XSS in po-web-ui fmt_currency: the non-numeric
fallback returned str(val) unescaped, so a prompt-injected email
could make Claude emit total_amount as <script>. Escape it.

Refs: INFRA-74
2026-06-17 11:37:49 -04:00
Adam Moussa
47fa35688d
fix(po): grant KMS on seahaven-dynamodb CMK to purchase-orders consumers (INFRA-104) (#56)
Some checks are pending
Deploy / deploy (push) Waiting to run
The purchase-orders table was migrated to SSE-KMS (alias/seahaven-dynamodb,
INFRA-95/M-3) out-of-band, but po_stack never declared the key, so
grant_read_write_data did not propagate kms perms. po-email-processor failed
~99.6% of invocations with kms:Decrypt AccessDeniedException, a data-loss
outage on the PO ingestion write path.

- po_stack: declare encryption_key on purchase-orders (reconciles SSE drift;
  no-op against the already-encrypted live table) so the existing grants add
  kms:Decrypt/GenerateDataKey/DescribeKey to EmailProcessor, WebUI, SiteExtractor.
- wo_stack: pre-emptive grant_encrypt_decrypt on the WO processor role ahead of
  the WorkOrders CMK migration (INFRA-6); tables left unencrypted, no table change.

GPT-4.1 cross-review: no blockers.
2026-06-10 19:31:55 -04:00
Adam Moussa
109c565cbf
Reconcile IaC with out-of-band DLQ + Function URL changes (INFRA-74, INFRA-41) (#50)
Some checks are pending
Deploy / deploy (push) Waiting to run
Make CDK the source of truth for two sets of changes applied out-of-band
via CLI to the po-ingest and WorkorderIngestStack stacks.

INFRA-74 (audit C-5): remove the public FunctionUrlAuthType.NONE Function
URL construct (and its auto-generated Principal:* invoke permission +
output) from both po-web-ui and workorder-web-ui. The URLs were already
deleted live via CLI; CFN's delete is idempotent.

INFRA-41 (audit H-8): add a CDK-managed SQS dead-letter queue
(dead_letter_queue=, 14d retention, SSL-enforced, CDK-generated name) and
an ALARM-only Errors alarm (Sum, threshold>0, site-alerts topic) for both
po-email-processor and workorder-email-processor, mirroring the
apm-wo-analysis-classifier DLQ and payments-payroll-batch alarm patterns.

Interim CLI resources (per-fn -dlq queues, -errors alarms, dlq-send inline
policies, OnFailure event-invoke-configs) removed post-deploy.
2026-06-08 16:02:29 -04:00
Adam Moussa
9370abf9ab
Drop three read-idle GSIs (audit M-20) (#35)
Some checks are pending
Deploy / deploy (push) Waiting to run
* Drop read-idle GSIs: by-state and status-index

Audit M-20: 30 days of CloudWatch metrics show 0 reads on both
indexes against 518 (by-state) and ~50k (status-index) WCU of write
amplification. No code path queries either index.

site-code-index follows in the next commit - CloudFormation allows
only one GSI change per table per deploy.

* Drop read-idle site-code-index GSI

Second half of the M-20 cleanup - deployed separately because
CloudFormation allows one GSI change per table per update.
2026-06-03 15:32:23 -04:00
Adam Moussa
a5d2bee748
Fix po-email-processor bundling for arm64 target (#34)
po-email-processor has been down since 2026-05-12: the bundling
command installed wheels for the build host's architecture, so CD
deploys from amd64 runners shipped x86_64 pydantic_core into an
ARM_64 function, crashing every INIT with Runtime.ImportModuleError.

Pin the pip platform to manylinux2014_aarch64 with --only-binary,
matching the pattern wo_stack already uses.

Verified live: deployed from branch, manual invoke OK; 920 emails
from the outage window backfilled via scripts/reprocess logic.
2026-06-03 14:56:02 -04:00
Adam Moussa
5112c1345b
Merge workorder-ingest into unified procurement repo (#22)
* Merge workorder-ingest pipeline into unified repo

Move PO lambdas under lambdas/po/, add WO pipeline under lambdas/wo/.
Two independent CloudFormation stacks in one CDK app. Fix WO stack
compliance: ARM64 architecture, 60-day log retention, aarch64 bundling,
RETAIN on Anthropic secret. Remove stale CodePipeline buildspec.

* Fix test_local.py import path and remove dead shared/models.py

test_local.py referenced the old lambdas/email_processor path. Updated
to lambdas/wo/email_processor. Removed shared/ directory entirely as
nothing imports from it.

* Escape HTML in both web UI dashboards to prevent XSS

Both Function URLs are public (auth_type=NONE) and render
email-derived content via f-strings. Attacker-crafted emails
could inject scripts. Added html.escape() on all interpolated
values in both PO and WO dashboards.

* Add pagination to WO web UI scan

get_work_orders() only fetched the first 1MB page from DynamoDB.
Loop on LastEvaluatedKey to match the PO web UI pattern.

* Fix esc(None) TypeError and javascript: scheme in PO web UI

Coerce supplier name through `or ""` before escaping to handle
nested None from DynamoDB. Add scheme allowlist on view_order_url
to block javascript:/data: hrefs from LLM-extracted URLs.

* Fix WO render_badge None guard, updated_at slice, and backfill path

Add null guard to WO render_badge matching the PO version. Use
`or ""` before slicing updated_at to handle explicit None values.
Fix backfill_sites.py sys.path to use new lambdas/po/site_extractor.

* Harden WO web UI and fix JS-context XSS in both dashboards

- Use json.dumps for onclick URLs to prevent JS string breakout
- Add .lower() to WO render_badge color lookup matching PO pattern
- Add pagination to get_comments query
- Cap get_work_orders to 500 results matching PO pattern

* Apply ruff formatting to web UI handlers
2026-05-12 15:21:06 -04:00
Renamed from cdk/stack.py (Browse further)