{"id":18,"date":"2025-12-09T07:39:21","date_gmt":"2025-12-09T07:39:21","guid":{"rendered":"https:\/\/steadyrabbit.in\/blogs\/?p=18"},"modified":"2025-12-09T11:01:45","modified_gmt":"2025-12-09T11:01:45","slug":"shift-left-isnt-just-for-qa-how-to-plan-left-measure-left","status":"publish","type":"post","link":"https:\/\/steadyrabbit.in\/blogs\/shift-left-isnt-just-for-qa-how-to-plan-left-measure-left\/","title":{"rendered":"Building an SBOM Pipeline That Developers Don\u2019t Hate"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">A software bill of materials (SBOM) is now table-stakes for supply-chain security, but bolting CycloneDX onto an already-slow CI\/CD is a sure way to spark dev revolt. This post shows how we generate, diff, sign, and publish SBOMs on every pull-request in &lt; 90 seconds\u2014and prove it with real latency numbers from a Micro-GCC squad maintaining an 8-service Node + Go platform. Copy-paste GitHub Actions included.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Why \u201cGenerate at Release\u201d <\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most teams produce a single SBOM artifact the night before release. That works\u2014until:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Dependency drifts<\/strong> between PR merge and release build.<br><\/li>\n\n\n\n<li><strong>Zero-days<\/strong> (e.g., Log4Shell) pop after freeze and you can\u2019t answer <em>\u201cWhich microservice is vulnerable?\u201d<\/em><em><br><\/em><\/li>\n\n\n\n<li>M&amp;A or customer security questionnaires ask for a <strong>signed SBOM per version<\/strong>, not per quarter.<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Our rule: <strong>SBOMs should appear as fast as SAST warnings.<\/strong> That means every PR, every image tag, every release.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Anatomy of a 90-Second SBOM Job (320 w)<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">yaml<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">jobs:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;sbom:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;runs-on: ubuntu-latest<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;timeout-minutes: 5<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;steps:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; uses: actions\/checkout@v4<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Set up Node<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;uses: actions\/setup-node@v4<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;with: node-version: &#8217;20&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Install deps (cached)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;uses: bahmutov\/npm-install@v1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Generate CycloneDX JSON<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;run: npx @cyclonedx\/bom -o sbom.json<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Diff against main<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;id: diff<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;run: |<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;echo &#8220;::set-output name=changed::$(git diff origin\/main sbom.json | wc -l)&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Upload to S3<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if: steps.diff.outputs.changed != &#8216;0&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;uses: jakejarvis\/s3-sync-action@v0.5.1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;with:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;args: &#8211;acl private &#8211;follow-symlinks &#8211;exact-timestamps<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; name: Sign SBOM<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;run: cosign attach sbom \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211;sbom sbom.json \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ghcr.io\/your\/repo:${{ github.sha }}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Latency breakdown (median):<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Step<\/strong><\/td><td><strong>Time<\/strong><\/td><\/tr><tr><td>npm-install (cache hit)<\/td><td>25 s<\/td><\/tr><tr><td>Generate JSON<\/td><td>12 s<\/td><\/tr><tr><td>Diff<\/td><td>1 s<\/td><\/tr><tr><td>S3 upload + Cosign sign<\/td><td>45 s<\/td><\/tr><tr><td><strong>Total<\/strong><\/td><td><strong>83 s<\/strong><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tip \u278a<\/strong>\u2003Cache dependencies by checksum, not lockfile date; 95 % of PRs reuse cache.<br><strong>Tip \u278b<\/strong>\u2003Skip S3 upload when diff == 0 to save 40+ seconds on doc-only commits.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Where to Store &amp; Sign SBOMs<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Model<\/strong><\/td><td><strong>Pros<\/strong><\/td><td><strong>Cons<\/strong><\/td><td><strong>When to Use<\/strong><\/td><\/tr><tr><td><strong>Container Attach (cosign)<\/strong><\/td><td>Co-located with image; verified by cosign verify<\/td><td>Only OCI images, not zip\/tar artifacts<\/td><td>Microservices on K8s<\/td><\/tr><tr><td><strong>Artifact Repo (S3 \/ MinIO)<\/strong><\/td><td>Works for any file; cheap<\/td><td>Requires URL mapping to version<\/td><td>Polyglot monorepos<\/td><\/tr><tr><td><strong>Git Tag<\/strong><\/td><td>Easy diffing<\/td><td>Bloats repo; binary in Git<\/td><td>Small libs or infra templates<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Signature standard:<\/strong> we use <strong>Sigstore\/cosign<\/strong>\u2014developers need <strong>zero key management<\/strong>; GitHub OIDC tokens issue short-lived certs.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Making Developers Care <\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Surface Value Fast<\/strong> \u2013 Add an <em>\u201cOpen SBOM\u201d<\/em> button to PR template. One click shows new deps.<br><\/li>\n\n\n\n<li><strong>Automate CVE Alerts<\/strong> \u2013 Hook Trivy or Dependency-Track to read latest SBOM and comment CVEs.<br><\/li>\n\n\n\n<li><strong>Gamify Size<\/strong> \u2013 Slack bot posts <em>\u201cSmallest SBOM delta of the week\u201d<\/em>\u2014nobody wants to be the bloat champ.<br><\/li>\n\n\n\n<li><strong>No Extra Tickets<\/strong> \u2013 SBOM job fails the build only for critical signer errors; warnings become PR comments.<br><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Result: devs see SBOM as <strong>their<\/strong> tool, not security theater.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Extending to Poly-Repo &amp; SAP Landscapes <\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Poly-Repo:<\/strong> push each SBOM to an S3 bucket with path \/service\/&lt;repo&gt;\/&lt;sha&gt;\/sbom.json. A Glue crawler builds an Athena table for org-wide queries:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">sql<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CopyEdit<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SELECT repo, COUNT(*) AS crits<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">FROM sbom_view<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WHERE severity = &#8216;CRITICAL&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GROUP BY repo<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ORDER BY crits DESC;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SAP ABAP:<\/strong> SAP CP orchestrates ABAP Git exports \u2192 Node job runs <strong>cyclonedx-bom<\/strong> on *.abapgit.xml. Attach SBOM as a transport artifact; ATC gate fails if missing.<strong>Edge case \u2013 binary blobs:<\/strong> use CycloneDX <em>component-hash<\/em> to reference fixed firmware. Store blob SBOMs in S3; link hash in SBOM externalReferences.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Real-World Impact (Case Snippet \u2013 Retail Client) (120 w)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Before SBOM pipeline<\/em>: release retro spent 4 hours on dependency review; Black-Friday freeze 3 days.<br><em>After<\/em>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SBOM job adds <strong>83 s<\/strong> to PR.<br><\/li>\n\n\n\n<li>CVE detection surfaced Log4Shell 20 min after CVE DB update\u2014patched same day.<br><\/li>\n\n\n\n<li>Black-Friday freeze shrank to half a day; no emergency patches in production.<br><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">PM\u2019s comment: <em>\u201cWe found vulnerabilities while they were still headlines, not headlines about us.\u201d<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Common Pitfalls &amp; Fixes (130 w)<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Pitfall<\/strong><\/td><td><strong>Fix<\/strong><\/td><\/tr><tr><td>\u201cCI time blew up\u201d<\/td><td>Cache deps; skip upload on no-diff; parallelise sign\/upload.<\/td><\/tr><tr><td>\u201cSBOM invalid JSON\u201d<\/td><td>Use CycloneDX CLI \u2265 v3.7; run cyclonedx validate.<\/td><\/tr><tr><td>\u201cToo many CVE false positives\u201d<\/td><td>Switch to OWASP dependency-track w\/ policy suppression YAML.<\/td><\/tr><tr><td>\u201cPrivate NPM packages missing\u201d<\/td><td>Add GitHub PAT with module read scope; CycloneDX CLI resolves them.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Take-Home Checklist <\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Install CycloneDX CLI in CI.<br><\/li>\n\n\n\n<li>Generate SBOM on every PR; fail build on syntax errors.<br><\/li>\n\n\n\n<li>Diff vs. main; sign &amp; upload only if changed.<br><\/li>\n\n\n\n<li>Wire Trivy\/Dependency-Track to auto-comment CVEs.<br><\/li>\n\n\n\n<li>Surface SBOM quick-view in PR template.<br><\/li>\n\n\n\n<li>Run org-wide Athena queries for supply-chain KPIs.<\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>A software bill of materials (SBOM) is now table-stakes for supply-chain security, but bolting CycloneDX onto an already-slow CI\/CD is a sure way to spark dev revolt. This post shows how we generate, diff, sign, and publish SBOMs on every pull-request in &lt; 90 seconds\u2014and prove it with real latency numbers from a Micro-GCC squad [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":20,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[11,10],"class_list":["post-18","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-shift-left-engineering","tag-ai","tag-chatgpt"],"_links":{"self":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/18","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/comments?post=18"}],"version-history":[{"count":2,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/18\/revisions"}],"predecessor-version":[{"id":34,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/posts\/18\/revisions\/34"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media\/20"}],"wp:attachment":[{"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/media?parent=18"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/categories?post=18"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/steadyrabbit.in\/blogs\/wp-json\/wp\/v2\/tags?post=18"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}