Skip to content

Security hardening, CLI bug fixes, and SARIF report output - #322

Open
DevamShah wants to merge 1 commit into
KeygraphHQ:mainfrom
DevamShah:security-bugs-features
Open

Security hardening, CLI bug fixes, and SARIF report output#322
DevamShah wants to merge 1 commit into
KeygraphHQ:mainfrom
DevamShah:security-bugs-features

Conversation

@DevamShah

Copy link
Copy Markdown

Drive-by contribution from a long-time Shannon user. I'm aware of the
"external PRs not accepted at this time" notice in the README — happy
to close this and refile via Issues / private channel if preferred,
or split into smaller pieces. Three orthogonal commits, each
independently revertible.

Summary

Three logically distinct commits, one PR for review convenience:

  1. security: hardening — prompt-injection defences in
    prompt-manager.ts, UID/GID validation in entrypoint.sh, drop
    chmod 777 to chmod 770 on container temp dirs.
  2. fix(cli): — URL try/catch + scheme allowlist on start,
    distinguish ENOENT from real I/O errors in the session-poll loop,
    locale-aware splash with ASCII fallback for terminals that don't
    render Unicode block art.
  3. feat: SARIF 2.1.0 report output — opt-in --report-format sarif
    flag wires a new SarifReportOutputProvider through the existing
    ReportOutputProvider DI seam. Default behaviour (md only) is
    byte-for-byte unchanged.

Why bundled

These were found while reading the codebase to evaluate Shannon for
internal use. Each commit stands alone — squash, cherry-pick, or close
any one without affecting the others.

Commit 1 — security

ID What Where
S-1 Prompt injection via config.description apps/worker/src/services/prompt-manager.ts
S-2 Credential injection (username / password / TOTP secret) into prompts same
S-3 config.avoid / config.focus rule descriptions injected raw same
S-4 SHANNON_HOST_UID / SHANNON_HOST_GID consumed by groupadd / useradd without numeric or range validation entrypoint.sh
S-5 chmod 777 on /app, /tmp/.cache, /tmp/.config, /tmp/.npm Dockerfile

Threat model. Anyone who can write a Shannon config (a CI secret
leak, a compromised target repo) can today embed {{AUTH_CONTEXT}}
or @include(/etc/passwd) in a description and have the agent treat
it as orchestrator-level instruction. After this change those payloads
are inert text — the new `sanitizePromptValue()` breaks {{...}}
placeholder syntax and @include(...) directives. Newlines are
preserved. Applied uniformly to every user-controlled interpolation
site.

`entrypoint.sh` now rejects non-numeric / out-of-range / zero values
for `SHANNON_HOST_UID` and `SHANNON_HOST_GID` with a clear error,
preventing a malicious env from mapping the pentest user to root or
feeding crafted input into a system command.

`chmod 770` is sufficient: only the pentest user (or a UID remapped
into the pentest group) ever runs in the container — world-write adds
blast radius without functional benefit.

Commit 2 — CLI bug fixes

  • `new URL(args.url)` was called twice deep in setup with no error
    handling; a malformed input crashed mid-setup with a raw `TypeError`.
    Now wrapped in try/catch with an explicit `http` / `https` scheme
    allowlist (the worker assumes web semantics).
  • The `session.json` polling loop's bare `catch` swallowed
    `EACCES` / `EIO` / `ENOTDIR` alike, so a permissions issue
    manifested as an indefinite "Waiting for workflow to start...".
    Now distinguishes `ENOENT` (steady-state) and `SyntaxError`
    (worker mid-write) from real I/O errors.
  • `splash.ts` falls back to a plain-ASCII variant when the terminal
    doesn't advertise UTF-8. Detection uses `LANG` / `LC_ALL` /
    `LC_CTYPE` plus the well-known `WT_SESSION` and
    `TERM_PROGRAM=vscode` signals. The Unicode visual is preserved on
    every modern terminal and only degrades on raw cmd.exe / locale-less
    SSH / some CI log streams.

Commit 3 — SARIF report output

`shannon start ... --report-format sarif` writes
`/.shannon/deliverables/comprehensive_security_assessment_report.sarif`
alongside the markdown report. The default (`md`) is unchanged.

Wiring goes:
`CLI flag → SHANNON_REPORT_FORMAT env → worker.ts:configureReportOutputProvider() → setContainerFactory(SarifReportOutputProvider)`

The provider plugs into the existing `ReportOutputProvider` interface
that's already invoked from `generateReportOutputActivity` —
zero changes to the activity / workflow layer.

Tool driver advertises five rules tagged with their CWE IDs and
the canonical OWASP help URI:
`shannon.injection` (CWE-74), `shannon.xss` (CWE-79),
`shannon.auth` (CWE-287), `shannon.ssrf` (CWE-918),
`shannon.authz` (CWE-285). Each non-empty
`*_exploitation_evidence.md` produces one SARIF `result` with the
evidence body as the message (truncated at 16 KiB to stay under
GitHub's per-result cap).

Out of scope (explicitly v0.1). Per-finding line/column locations
inside source files. The agents don't currently emit structured
per-finding metadata, so the artefact location is the deliverable file
itself. The envelope and consumer wiring shipped here unblock that
follow-up — the result mapping is then a one-function change.

Test plan

  • `pnpm test` — 21/21 vitest cases green across three suites:
    • `prompt-manager.test.ts` (11 cases) pins the
      `sanitizePromptValue` and URL validation contracts
    • `uid-gid-validation.test.ts` (5 cases) pins the regex+range
      contract that `entrypoint.sh` enforces, so the bash and TS
      sides can never silently drift
    • `sarif-output-provider.test.ts` (5 cases) covers SARIF envelope
      shape, one-result-per-evidence, empty/missing handling,
      oversized truncation, and the output path
  • `pnpm check` — `tsc --noEmit` clean for both `@keygraph/shannon` and `@shannon/worker`
  • `pnpm build` — turbo build succeeds; `apps/cli/dist/index.mjs`
    bundle 53.21 kB / 14.48 kB gzipped (no growth from main)
  • Manual: `node apps/cli/dist/index.mjs help` shows the new flag;
    `LANG=C node apps/cli/dist/index.mjs info` renders the ASCII
    fallback splash; `pnpm check` / `pnpm build` / `pnpm test` all
    cached on second run
  • Pre-existing biome issues on `main` (import-organize warnings in
    `workflows.ts` etc.) are not touched. All files added or modified
    in this PR pass `biome check` cleanly.

Things I deliberately did not do

  • No `exec $*` quoting fix in `entrypoint.sh`. The args come
    from Docker `CMD`, not user input — the practical injection risk is
    near zero, and the fix is fiddly enough that I'd rather not bundle
    it.
  • No `--max-cost` cap or `--dry-run` mode. Both were on my
    shortlist but each needs cost-tracker / fs-write-audit infrastructure
    I don't yet have a clean read on. Happy to follow up with separate
    PRs if there's interest.
  • No SARIF result locations inside source files. Out of scope until
    agents emit structured findings (see Commit 3 description).

Built against `main @ 79caada` (post-v1.1.0). Author: Devam Shah —
no commercial affiliation, contributing personally.

DevamShah added a commit to DevamShah/vedha that referenced this pull request Apr 27, 2026
Adds an opt-in SARIF emission path so Vedha findings can be consumed
directly by GitHub Code Scanning, GitLab, Defect Dojo, and any other
SARIF-aware scanner UI. Also rewrites the README to credit upstream
Shannon clearly and enumerate exactly what Vedha layers on top.

Behaviour:

  ./vedha start -u ... -r ... --report-format sarif

writes
  <repo>/.shannon/deliverables/comprehensive_security_assessment_report.sarif

alongside the existing markdown report. The default (`--report-format md`)
is unchanged byte-for-byte.

Wiring:

- apps/cli: new `--report-format md|sarif` flag on `start`. Validated
  up front. Help text describes the two values.
- apps/cli/docker.ts: forwards VEDHA_REPORT_FORMAT and VEDHA_VERSION
  to the worker container as env. Env is the right channel because the
  worker reads them inside `assembleReportActivity` to gate optional
  emission, and env survives Temporal serialisation without needing
  pipeline-input plumbing.
- apps/worker/temporal/activities.ts: `assembleReportActivity` now
  invokes SarifReportOutputProvider after the markdown assembly when
  VEDHA_REPORT_FORMAT=sarif. SARIF emission is best-effort — a failure
  there never blocks the markdown path.
- apps/worker/services/sarif-output-provider.ts: new provider. Walks
  the five `*_exploitation_evidence.md` deliverables that
  `assembleFinalReport` already consumes; emits one SARIF result per
  non-empty evidence file with the body as the result message
  (truncated at 16 KiB).
- apps/worker/services/index.ts: re-exports SarifReportOutputProvider.
- apps/worker/tsconfig.json: excludes __tests__/** from production
  compile so test code doesn't end up in dist/.

Tool driver advertises five rules tagged with their CWE IDs:
  vedha.injection (CWE-74), vedha.xss (CWE-79), vedha.auth (CWE-287),
  vedha.ssrf (CWE-918), vedha.authz (CWE-285).

Test infrastructure:
- vitest dev dep + `test` script on @shannon/worker
- vitest.config.ts in apps/worker
- turbo `test` task wired up
- 5 new SARIF tests covering envelope shape, one-result-per-evidence,
  empty/missing handling, oversized truncation, output path
- Total: 29/29 pass (24 existing + 5 new)
- pnpm check + build clean

README:
- New "Credit & lineage" section: Vedha is a fork of Shannon by
  Keygraph. Architecture is theirs; Vedha exists to carry security
  hardening, propose improvements upstream, and integrate with the
  Archeon stack.
- New "What Vedha adds over upstream Shannon" section enumerating
  all 8 security fixes (S-1..S-8) and the SARIF feature.
- Versioning policy table linking Vedha versions to Shannon base.
- Cross-link to KeygraphHQ/shannon#322 (the upstream PR carrying
  the security hardening for review).

Out of scope for v1.1.0 (deferred follow-ups):
- --max-cost USD kill switch
- --dry-run / read-only mode
- Per-finding line/column SARIF locations (needs structured findings
  from agents)
Two container-runtime issues, both on the path that remaps the in-container
pentest user to the host's uid.

entrypoint.sh: SHANNON_HOST_UID and SHANNON_HOST_GID reach groupadd/useradd
unvalidated. An empty GID (set UID without GID) fails with an opaque
"groupadd: invalid group ID ''"; a UID of 0 silently maps the agent user onto
root, which defeats running the agents unprivileged in the first place. Both
are now checked against ^[0-9]+$ and a 1..2000000 range before use, with an
explicit error. Validation moved inside the remap branch so the no-env path
is byte-for-byte unchanged.

Dockerfile: /app, /tmp/.cache, /tmp/.config and /tmp/.npm are chmod 777. Only
the pentest user (or a remapped uid running as pentest) ever writes them, so
world-writable adds blast radius with no functional benefit. Dropped to 770.

The 777 was, however, load-bearing for the remap path: the entrypoint chowns
/app/sessions, /app/workspaces, /tmp/.claude and /tmp/.pi after remapping but
never /tmp/.cache, /tmp/.config or /tmp/.npm, which stayed writable only
because they were world-writable. Tightening to 770 without also extending
that chown would break `SHANNON_HOST_UID` runs, so the entrypoint's chown list
now covers those three directories too.

Signed-off-by: devamshah <devamshah91@gmail.com>
@DevamShah
DevamShah force-pushed the security-bugs-features branch from dc82513 to a650708 Compare August 28, 2026 08:08
@DevamShah

Copy link
Copy Markdown
Author

@ezl-keygraph — this sat for four months at +1677/-40 because I bundled three unrelated things into one PR. That was my mistake, so I've cut it down rather than ask you to wade through it.

Dropped, and why:

  • SARIF output — you shipped it in feat: multi-provider model support, SARIF output, and exploit-mode fixes #402 and made it the default for exploit runs in fix(report): emit SARIF by default for exploit runs #431. Fully superseded; deleted.
  • Prompt-interpolation sanitiserreplaceLiteral, plus running processIncludes before interpolateVariables, already closes most of what I was guarding against. Not worth the diff.
  • ASCII splash fallbacksplash.ts:3 now states the Unicode art is deliberately always kept. Your call; dropped.
  • vitest + a turbo test task — the repo has no test framework today, and adopting one is a decision for you to make, not something to smuggle in under a security PR.

What's left is rebased onto main (a650708) at +26/-6 across two files: reject non-numeric / 0 / out-of-range SHANNON_HOST_UID and SHANNON_HOST_GID before they reach groupadd/useradd, and drop chmod 777770 on /app, /tmp/.cache, /tmp/.config, /tmp/.npm.

Worth flagging, because it's the non-obvious half: the 777 is load-bearing today. entrypoint.sh re-chowns /app/sessions, /app/workspaces, /tmp/.claude and /tmp/.pi after a uid remap, but never the three /tmp cache dirs — they stay writable by the remapped uid only because they're world-writable. Tightening the mode alone would break every SHANNON_HOST_UID run, so the commit extends that chown as well.

Verified: docker build --check .Check complete, no warnings found. Validation exercised directly against entrypoint.sh0, -1, abc, 1001; rm -rf /, 3000000, and an empty GID each exit 1 with the error; 1000/1000 passes and proceeds to groupadd; unset skips the remap branch entirely, unchanged. I have not built the full image or run a container with a remapped uid end-to-end.

Two things I pulled out and deliberately did not open PRs for — say the word if either is wanted:

  • apps/cli/src/commands/start.ts:235 — the session.json poll swallows every error in a bare catch {}, so an EACCES or EIO surfaces as 120s of "Waiting for the scan to start" followed by a timeout, with no diagnostic.
  • apps/cli/src/index.ts:138new URL(url) validates parseability but not scheme, so file:// and javascript: pass.

The title and description still describe the old three-part scope; treat this comment as the description until I correct them.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant