Skip to content

Enterprise security enforcement: one configuration and rulesets for every repo #94

Description

@bryanfawcett

Owner, 2026-10-07: "We need security to be properly enforced across the entire enterprise." This follows the shift-left audit: an issue was opened in all 83 repos, "Shift-left security: what this repo runs today, and the gaps".

Baseline (2026-10-07, 83 repos in the 7 enterprise orgs; nyuchiGOV excluded)

Check Repos with it
Secret scanning + push protection 83
CodeQL on code languages 55 (22 more scan only actions)
Dependabot security updates 54
Dependency review / audit in CI 9 / 10
Pre-commit hooks 5
Semgrep 0
DAST / chaos 0
Editor config 1

The only org-required check is org-lint.yml (lint plus Vite+), which contains no security check.

Target: enforced from the enterprise, not copied per repo

  1. An enterprise security configuration (code security and secret protection), applied to every repo in the 7 orgs and enforced, so repos can't opt out:
    • dependency graph; Dependabot alerts and security updates;
    • CodeQL default setup on all supported languages;
    • secret scanning, push protection and validity checks;
    • private vulnerability reporting.
  2. Enterprise rulesets on the default branch and staging:
    • a code_scanning rule (CodeQL: block on high/critical alerts and on high-severity security issues);
    • a required dependency review workflow;
    • a required Semgrep workflow on changed files, from nyuchi/.github.
      Start each in evaluate mode, then switch to active once green.
  3. Pre-commit: an org-standard hook config (Semgrep on changed files, plus lint) shipped through the Mzizi dev skills and repo templates. The CI version of the same check is what gets enforced; the hook is the fast local copy.
  4. Editor: a standard .vscode/extensions.json (Semgrep, SonarQube for IDE, and the repo's linters) through the same path.
  5. DAST and chaos: OWASP ZAP baseline against every deployed beta/staging host, plus Mzizi chaos (fault injection) in e2e. These apply per deployed service, from reusable workflows.
  6. Track to done: close each repo's shift-left issue as it goes green.

The Toddle extension is the pilot for steps 3–5 (nyuchi/toddle-enhancement-extension#64).

Status

  • Security configuration created and enforced on all 7 orgs
  • Code scanning ruleset (evaluate, then active)
  • Dependency review required workflow (evaluate, then active)
  • Semgrep required workflow (evaluate, then active)
  • Pre-commit and editor standard shipped
  • DAST/chaos reusable workflows; per-service rollout
  • Per-repo issues closed

Activity

  1. bryanfawcett commented on Oct 7, 2026

    @bryanfawcett
    MemberAuthor

    Step 1 done: enterprise security configuration, enforced

    Configuration: Bundu enterprise security baseline (enterprise config id 281854, enforcement enforced). View

    Feature Setting
    Dependency graph enabled
    Dependabot alerts enabled
    Dependabot security updates enabled (new: config 17 had it not_set)
    CodeQL default setup enabled, standard runners, advanced setup allowed where a repo already runs CodeQL itself
    Secret scanning, push protection, validity checks, non-provider patterns, extended metadata enabled
    Private vulnerability reporting enabled (GitHub offers it on public repos only)
    Code Security + Secret Protection (GHAS) enabled
    • Attached to all 89 repos in the 7 orgs (scope all). All 89 report enforced, including bundu-labs/marketing, which had failed under the old "GitHub recommended" config.
    • Set as the enterprise default for new repos (all), replacing "GitHub recommended" (id 17). Config 17 was not deleted.
    • Nothing was switched off. I snapshotted security_and_analysis on all 89 repos before and after attaching, and no enabled feature changed. Dependabot security updates went from 50 repos to 86. The other 3 are archived and read-only: mukoko-dev/mukoko-auth, nyuchi/iconic-expeditions-landing and nyuchi/r2-brand-explorer.
    • Verified on nyuchi/toddle-enhancement-extension: CodeQL default setup covers actions, javascript-typescript; Dependabot alerts and security updates are on; secret scanning, push protection and validity checks are on.

    Licensing and cost

    GHAS on private repos is metered and already active. Before this change: Code Security had 1 active committer across 33 repos and Secret Protection 1 across 32. The old config already enabled GHAS on every private repo, so attaching the new one added no billable repos or committers. At list price that is US$30 (Code Security) plus US$19 (Secret Protection) per active committer per month, so about US$49 a month today. Each extra person who commits to a private repo adds about US$49 a month. Nothing was bought.

    Open: CodeQL language gaps (owner)

    Now that the configuration is enforced, a repo's default-setup languages can only be changed in the UI. Through the API the language list rejects rust, so Rust can't be added that way at all. My attempt to lift enforcement briefly, re-run detection and re-enforce was denied by the permission system, so I left it there. These repos run CodeQL on fewer languages than they contain:

    • nyuchi/nyuchi-platform: only actions; also has TypeScript, Rust and Astro.
    • nyuchi/nyuchi-docs: only actions; also has TypeScript and Astro.
    • nyuchi/toddle-enhancement-extension: python missing (a small amount of Python).
    • mzizi-dev/mzizi-registry: java-kotlin and swift missing (Kotlin and Swift present).

    Fix, per repo: Settings → Code security → CodeQL → Edit, then tick the languages.

    Rust is covered by default setup elsewhere: bundu-standards, kweli, mukoko-circles, mukoko-weather, nyuchi-identity, super-app-web, mzizi, mzizi-console, mzizi-registry, workspace-tools, openNTL/ntl, siafudb and siafudb-kuzu.

    Not covered: .astro and .svelte files themselves. CodeQL's JavaScript extractor doesn't read them, only the .ts/.js they import. The Semgrep workflow (step 2c) extracts Astro frontmatter and <script> blocks to cover this. PHP has no CodeQL support (nyuchi/auto-seo-manager, seo-manager, nyuchi-travel-addons).

  2. bryanfawcett commented on Oct 7, 2026

    @bryanfawcett
    MemberAuthor

    Step 5: DAST and chaos, the design (nothing built yet)

    1. nyuchi/.github reusable-zap-baseline.yml

    • Triggers: workflow_call (from a repo's deploy workflow, after its beta or staging deploy succeeds), plus a weekly schedule in each repo.
    • Inputs:
      • target-url (required, https only);
      • rules-file (optional, default .zap/rules.tsv in the caller repo, so known false positives can be set to IGNORE or WARN);
      • fail-on: high (default) or medium;
      • ajax-spider (bool, default false; true for SPA hosts);
      • max-minutes (default 10).
    • Job:
      • Run zaproxy/action-baseline, pinned by SHA, with the ghcr.io/zaproxy/zaproxy:stable image pinned by digest, cmd_options: -a -j -m <max-minutes> -I, allow_issue_writing: false and fail_action: false.
      • Then a small step parses report_json.json and fails on any alert at or above fail-on that the rules file doesn't IGNORE.
      • Reports (report_html.html, report_md.md, report_json.json) are uploaded as an artifact (retention 14 days), and the markdown goes to the job summary.
      • No SARIF upload at first, because ZAP's alerts don't map cleanly to code locations. Revisit when the evaluate numbers are in.
    • Baseline only, never the active scan. A baseline scan spiders the site and checks it passively, with no attack traffic. That matters because the betas share production D1, KV, Mongo and the Nyuchi API (beta-shares-prod-bindings), so an active zap-full-scan against a beta would write junk into production data. An active scan only runs against a host with its own isolated data, and today there is none. That decision is the owner's.
    • Auth: none at first; public surfaces only. An authenticated scan later needs a test account per app (WorkOS), with the session held in an org secret and passed through ZAP's replacer. That's an owner step.
    • Pinned and token-free: no ZAP cloud, and GITHUB_TOKEN gets contents: read only.

    2. Where it runs: repos with a deployed non-production host

    Verified today from GitHub deployment statuses, with a curl of each stable host.

    Stable beta or staging hosts (scan these first):

    Repo Host
    mzizi-dev/mzizi-docs https://docs.mzizi.dev (staging env, 200)
    nyuchi/mintlify-docs https://docs.nyuchi.com (staging env, 200)
    shamwari-ai/docs https://docs.shamwari.ai (staging env, 200)
    siafuDB/siafudb, siafuDB/docs https://siafudb.org (staging env, 308)
    openNTL/ntl https://openntl.org (staging-docs env, 308)
    bundu-labs/zimbabwe-information https://www.travel-info.co.zw (staging env, 308)
    mukoko-dev/kweli beta env, deploys to a Vercel URL per build
    nyuchi/nyuchi-platform staging – nyuchi-console-rs env, Vercel URL per build

    Vercel previews only (scan the preview URL from the deployment_status event):

    • bundu-labs/marketing: 6 projects
    • mukoko-dev: mukoko, mukoko-news, mukoko-lingo (+ console), nyuchi-identity, mukoko-events, mukoko-home, bushtrade, mukoko-weather, mukoko-events-admin
    • mzizi-dev/mzizi-registry
    • nyuchi: learning, Nyuchi-rentals, zti-app, agentgateway, nyuchi-platform (nyuchi-console)
    • shamwari-ai/shamwari: platform and web

    For these, a caller workflow runs on: deployment_status (state success, environment not Production) and passes github.event.deployment_status.environment_url, after checking that it matches ^https://[a-z0-9-]+-nyuchi(tech)?\.vercel\.app$. Vercel Deployment Protection, if it's on, needs a bypass token as an org secret (owner step).

    Not yet deployed:

    • beta.mukoko.com and app.mukoko.com (super-app-web) don't resolve today.
    • beta.events/news/weather.mukoko.com don't resolve either.
    • Add them when they go live.
    • APIs (api.nyuchi.com, api.mzizi.dev) need ZAP's zap-api-scan against their OpenAPI documents. That's a second workflow, reusable-zap-api.yml, in passive mode for the same production-data reason.

    Rollout:

    1. The pilot is nyuchi/toddle-enhancement-extension#64, handled by its own agent. It has no host, so it pilots the chaos part and the pre-commit and editor standard.
    2. Then the four docs hosts, which are static and lowest risk.
    3. Then the Vercel previews, one org at a time.
    4. Each runs in report-only mode for two weeks (fail-on set above high via a report-only: true input), then fails on high.

    3. Chaos in e2e: Mzizi chaos

    https://api.mzizi.dev/v1/ui/chaos (install with npx shadcn@latest add https://api.mzizi.dev/v1/ui/chaos; depends on observability) provides:

    • withChaos(fn, { enabled, errorRate, latencyMs }), which injects random errors and latency;
    • a middleware form with fixed configuration;
    • ChaosError.

    It is disabled by default.

    • Where: in e2e (Playwright) suites only. Wrap the app's outbound calls (Nyuchi API client, provider fetches) with withChaos, gated on process.env.MZIZI_CHAOS === "1", which only the e2e job sets on a preview or local build. It's never enabled in production, and never on a beta, because betas share production bindings.
    • Suite: a chaos Playwright project re-runs the critical journeys (sign-in, the main read path, one write path) at errorRate: 0.3 and latencyMs: [200, 1500], and asserts on behaviour, not success:
      • error boundaries render;
      • retries and fallbacks fire;
      • nothing hangs past the timeout;
      • no unhandled rejection appears in the console;
      • a seeded run is reproducible: the error decision should come from an injectable RNG, which the registry component lacks today. Per the Mzizi-upstream rule, that goes upstream to mzizi-registry as chaos with a seed option and a contract test, not a local fork.
    • Reusable: nyuchi/.github reusable-e2e-chaos.yml installs, builds, starts the preview or local server and runs playwright test --project chaos. Runs are nightly and on PRs labelled chaos. It's advisory until each app's chaos journeys are stable.
    • Rust services: the same idea as a tower layer behind a cargo feature chaos, never in release builds. That needs a mzizi-roots crate counterpart, which also goes upstream.

    Owner decisions for step 5

    1. Baseline-only against betas that share production data. Recommended: yes. Isolated data is needed before any active scan.
    2. Test accounts and secrets for authenticated scans, and a Vercel protection bypass, if previews are protected.
    3. Whether to give super-app-web a real beta host (beta.mukoko.com doesn't resolve).
  3. bryanfawcett commented on Oct 7, 2026

    @bryanfawcett
    MemberAuthor

    Step 2 done: three new enterprise rulesets, all in evaluate mode

    They use the same conditions as enterprise-release-version-check: every org, every repo except sandbox-* and archive-*, refs ~DEFAULT_BRANCH and refs/heads/staging. Enterprise owners can bypass. None of them is active.

    Ruleset id Rule
    enterprise-code-scanning 24653328 code_scanning: CodeQL, alerts threshold errors, security alerts high_or_higher
    enterprise-dependency-review 24655436 required workflow nyuchi/.github .github/workflows/dependency-review.yml @ refs/heads/main
    enterprise-semgrep 24655437 required workflow nyuchi/.github .github/workflows/reusable-semgrep.yml @ refs/heads/main

    Workflows

    #95 added the workflows; it was merged to staging under the merge gate (4 review rounds, the last one clean, and runtime-tested in #96). #97 released it to main.

    • dependency-review.yml, two jobs:
      • review: dependency-review-action v5.0.0, pinned; fails on new high or critical advisories. The dependency graph covers npm, pnpm, Yarn, Cargo, pip, Poetry and uv.
      • audit: runs only on the lockfiles a change touches.
        • npm audit and pnpm audit fail on high and above.
        • cargo audit fails on any RustSec advisory.
        • pip-audit runs on uv.lock and requirements*.txt.
        • Each auditor runs on copies of the files in an empty directory, so a PR's .npmrc, .pnpmfile.cjs, pnpm.auditConfig or .cargo/audit.toml can't silence it or run code.
      • It passes when nothing changed. Every action is pinned to a commit.
    • reusable-semgrep.yml: the open-source Semgrep CLI 1.179.0, image pinned by digest. No token, metrics off, --error.
      • Diff-aware on pull requests and merge queue groups: --baseline-commit against the base, so only new findings count.
      • Packs: p/typescript p/javascript p/react p/python p/rust p/secrets p/owasp-top-ten, plus a repo's own .semgrep.yml.
      • Astro: the frontmatter and <script> blocks of changed .astro files are extracted to .ts files (line numbers kept), and both base and head versions are scanned with the same baseline. test: planted findings for the security workflows (do not merge) #96 caught a hard-coded JWT secret in Astro frontmatter this way.

    Coverage by stack

    Stack CodeQL (config 281854) Semgrep Dependency audit
    TypeScript / JavaScript / React javascript-typescript p/typescript, p/javascript, p/react review + npm / pnpm audit
    Astro only the .ts it imports; .astro files aren't extracted by CodeQL frontmatter and scripts extracted as TS
    Rust rust (default setup, where detected) p/rust review + cargo audit
    Python python p/python review + pip-audit (uv.lock, requirements)
    Mzizi repos as above, per language as above as above
    Svelte the imported TS only not extracted (gap) as TS
    Yarn / Bun / Poetry lockfiles – – review only (no second auditor)
    PHP (3 nyuchi repos) no CodeQL support no PHP pack enabled review only

    Next: step 3. The numbers will be measured from rule-suite insights once real PRs and merges have run against these rulesets. Nothing will be activated.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions