Posts

Dependency Risk, SBOMs, and Automated Security for Angular

Build an SBOM and CI security pipeline for Angular monorepos: npm audit, ng update --dry-run, fail on high-severity CVEs, Dependabot/Snyk, and post summaries to Slack or Mastodon.
Table of Contents

Angular apps are rarely “just Angular.” They sit on the CLI, a pile of third-party libraries, often Nx or another monorepo tool, and a lockfile that quietly doubles every quarter. That surface is where a lot of real risk lives: not in your component tree, but in a transitive package nobody reviewed last sprint.

Best-practice write-ups keep repeating the same triad — scan regularly, automate checks in CI, and treat framework updates as security work. The missing piece for many teams is turning that advice into a pipeline that generates an SBOM, fails on high-severity CVEs, and tells humans when something broke — without waiting for someone to remember npm audit on Friday.

This post is that pipeline: SBOM generation, npm audit + ng update --dry-run, severity gates, Dependabot/Snyk as continuous PR pressure, and a small notification step that can land in Slack or Mastodon.

The Short Version

  1. Know what you ship — generate a CycloneDX or SPDX SBOM from the lockfile on every CI run and archive it.
  2. Scan continuouslynpm audit (or Snyk) in CI; Dependabot/Renovate for PR-driven bumps.
  3. Watch the frameworkng update --dry-run so you see Angular CLI / core drift before it becomes a CVE fire drill.
  4. Fail closed on high severity — treat critical/high findings as build failures; triage medium/low with a backlog.
  5. Notify people — post a short summary to Slack or Mastodon when the gate fails or when the SBOM changes in a scary way.

CSP and auth patterns close XSS and token theft. Dependency hygiene closes the supply-chain door those attacks often walk through. Pair this with framework security updates and CSP for Angular.

Why Angular Monorepos Amplify Dependency Risk

A solo ng new app already pulls hundreds of packages. An Nx workspace multiplies that by apps, libs, and shared tooling. One vulnerable transitive dependency can land in every deployable at once.

Typical failure modes:

  • Stale Angular major — you are still on a release train that already shipped security advisories.
  • Forgotten peer ranges — a UI kit pins an old rxjs or zone.js while the rest of the repo moved on.
  • DevDependency complacency — “it’s only build-time” until a compromised postinstall script runs in CI with cloud credentials.
  • No inventory — when a CVE hits, nobody can answer “which apps use this package?” without grepping lockfiles by hand.

An SBOM (Software Bill of Materials) is the boring answer to that last problem: a machine-readable list of what is in the build, so scanners and humans share the same inventory.

Generate an SBOM in CI

For npm/pnpm/yarn lockfiles, CycloneDX and Syft are the usual starting points. CycloneDX’s npm plugin is enough for most Angular repos:

1
npx @cyclonedx/cyclonedx-npm --output-file sbom.json --output-format json

In GitHub Actions, archive it as a build artifact (and optionally upload to your vulnerability platform):

1
2
3
4
5
6
7
8
- name: Generate SBOM
  run: npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.json

- name: Upload SBOM
  uses: actions/upload-artifact@v4
  with:
    name: sbom
    path: sbom.json

For Nx monorepos, run SBOM generation at the workspace root (the lockfile that actually resolves installs), not once per project, unless you publish independent packages with different graphs.

Minimal Angular Security Job

Here is a practical GitHub Actions workflow that:

  1. installs dependencies,
  2. runs npm audit and fails on high/critical,
  3. runs ng update --dry-run for visibility,
  4. generates an SBOM,
  5. posts a summary when the audit gate fails.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
name: angular-dependency-security

on:
  push:
    branches: [main]
  pull_request:
  schedule:
    - cron: "0 6 * * 1" # Mondays — catch advisory lag

jobs:
  dependency-hygiene:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm

      - name: Install
        run: npm ci

      - name: npm audit (fail on high+)
        id: audit
        run: |
          set +e
          npm audit --audit-level=high --json > audit.json
          status=$?
          set -e
          echo "audit_exit=$status" >> "$GITHUB_OUTPUT"
          if [ "$status" -ne 0 ]; then
            echo "::error::npm audit found high or critical vulnerabilities"
            npm audit --audit-level=high || true
            exit "$status"
          fi

      - name: Angular update dry-run
        run: npx ng update --dry-run || true
        # Informative: does not fail the job. Track output in CI logs / summary.

      - name: Generate SBOM
        if: always()
        run: npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.json

      - name: Upload SBOM
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom.json

      - name: Job summary
        if: always()
        run: |
          {
            echo "## Dependency security"
            echo "- Audit exit: \`${{ steps.audit.outputs.audit_exit || 'n/a' }}\`"
            echo "- SBOM: \`sbom.json\` artifact"
            echo "- \`ng update --dry-run\` logged above"
          } >> "$GITHUB_STEP_SUMMARY"

GitLab CI is the same idea with different syntax:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
dependency-hygiene:
  image: node:22
  script:
    - npm ci
    - npm audit --audit-level=high
    - npx ng update --dry-run || true
    - npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.json
  artifacts:
    paths:
      - sbom.json
    when: always

What --audit-level=high buys you

npm audit without a level often fails the build on a pile of low/moderate noise. Starting with high (and critical) keeps the gate meaningful. Medium findings still belong in Dependabot PRs and backlog grooming — they just should not block every merge on day one.

If you use Snyk instead (or in addition), keep the same severity contract: fail the pipeline on high+, open PRs for the rest.

Continuous Pressure: Dependabot (and Friends)

CI that only fails is half a system. You also need a bot that opens upgrade PRs while the CVE is still fresh.

Minimal Dependabot config for an npm Angular workspace:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly
    open-pull-requests-limit: 10
    groups:
      angular:
        patterns:
          - "@angular/*"
          - "@angular-devkit/*"
          - "@schematics/*"
      angular-eslint:
        patterns:
          - "angular-eslint"
          - "@angular-eslint/*"

Grouping @angular/* keeps major upgrades reviewable as one PR instead of twenty. Pair that with a weekly ng update review on the platform team calendar — Dependabot bumps packages; ng update still owns migrations and schematics.

Snyk’s Angular guidance pushes the same shape: continuous monitoring plus PR fixes, not a quarterly spreadsheet. Use whichever vendor your org already pays for; the pipeline contract stays identical.

Optional: Angular-Specific Anti-Pattern Checks

Dependency scanners catch known CVEs. They do not catch bypassSecurityTrustHtml sprawl, inline event handlers that fight CSP, or tokens in localStorage. Those belong in a thin SAST or custom lint layer — for example ESLint rules, a small AST/scanner script (see the companion idea in the CSP post), or Semgrep patterns your security team owns.

Keep that job separate from npm audit so a flaky custom rule does not hide a real CVE signal.

Notify Slack — or Mastodon

When the audit gate fails, someone has to see it outside the Actions tab. Slack is the corporate default. Mastodon (or any ActivityPub endpoint with a bot account) is a nice fit if you already care about decentralized social tooling and want public or team-visible status without another SaaS.

Slack incoming webhook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- name: Notify Slack on audit failure
  if: failure() && steps.audit.outcome == 'failure'
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
  run: |
    curl -sS -X POST -H 'Content-type: application/json' \
      --data "{
        \"text\": \"Angular dependency gate failed on \`${GITHUB_REPOSITORY}@${GITHUB_SHA:0:7}\`. Check Actions + SBOM artifact.\"
      }" \
      "$SLACK_WEBHOOK_URL"

Mastodon status (API)

Create an app token on your instance, store it as MASTODON_TOKEN, and post a short status:

1
2
3
4
5
6
7
8
9
- name: Notify Mastodon on audit failure
  if: failure() && steps.audit.outcome == 'failure'
  env:
    MASTODON_TOKEN: ${{ secrets.MASTODON_TOKEN }}
    MASTODON_INSTANCE: ${{ vars.MASTODON_INSTANCE }} # e.g. https://mastodon.social
  run: |
    curl -sS -X POST "${MASTODON_INSTANCE}/api/v1/statuses" \
      -H "Authorization: Bearer ${MASTODON_TOKEN}" \
      -F "status=Dependency gate failed: ${GITHUB_REPOSITORY}@${GITHUB_SHA:0:7} — high/critical npm audit. SBOM attached in CI."

Keep messages boring and actionable: repo, short SHA, which gate failed. Do not dump full npm audit JSON into a public Mastodon timeline — link to the private CI run instead if the repo is private.

A Practical Checklist

Use this as a CI security checklist from “we have Angular” to “we can answer auditors”:

StepCommand / toolGate?
Lockfile installnpm ciYes — never npm install in CI
Vulnerability scannpm audit --audit-level=high or SnykYes on high+
Framework driftng update --dry-runNo (warn / summary)
SBOMCycloneDX / SyftArchive always
Automated PRsDependabot / Renovate / SnykReview weekly
App footgunsESLint / custom scanner / SemgrepTeam policy
Human signalSlack or Mastodon webhookOn failure

What This Does Not Replace

  • Patching Angular itself when advisories ship — scanners help you notice; ng update still has to land.
  • Server-side authorization — interceptors and guards are UX, not your security boundary (modern auth patterns).
  • Browser defenses — CSP and sanitization still matter when a dependency or CMS field goes wrong.

Dependency automation is the inventory and the smoke alarm. You still have to leave the building when it rings.

Final Thought

The Angular ecosystem makes it easy to assemble a powerful frontend — and easy to inherit a powerful attack surface. An SBOM plus a boring CI job (npm audit, ng update --dry-run, fail on high severity, notify somewhere humans actually look) turns “we should scan more” into a default. Start there. Layer Snyk or Dependabot for continuous PRs. Add Angular-specific SAST when the CVE noise is under control.

That is dependency hygiene as an engineering system, not a quarterly panic.

Further Reading & References