<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hariharan</title>
    <description>The latest articles on DEV Community by Hariharan (@pkkht).</description>
    <link>https://dev.to/pkkht</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1050851%2F6b07e8f8-7293-42ef-ac24-0718ae0654fc.jpeg</url>
      <title>DEV Community: Hariharan</title>
      <link>https://dev.to/pkkht</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pkkht"/>
    <language>en</language>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 6: The Full Pipeline</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:23:18 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-6-the-full-pipeline-2kd2</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-6-the-full-pipeline-2kd2</guid>
      <description>&lt;p&gt;If you've followed along from Part 1, we have built five separate scanning&lt;br&gt;
workflows. This final part replaces them with a single unified pipeline —&lt;br&gt;
one YAML file, one run, everything in the right order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline structure&lt;/strong&gt;&lt;br&gt;
The five individual workflow files are deleted and replaced with one:&lt;br&gt;
&lt;code&gt;.github/workflows/devsecops-pipeline.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DevSecOps Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;secret-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret Scanning - Gitleaks&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Gitleaks&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;

  &lt;span class="na"&gt;sast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAST - Bandit&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-scan&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Bandit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install bandit&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Bandit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit -r app.py --severity-level high -f json -o bandit-report.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit-report.json&lt;/span&gt;

  &lt;span class="na"&gt;sca&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SCA - pip-audit&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-scan&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install pip-audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install pip-audit&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run pip-audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit -r requirements.txt -f json -o pip-audit-report.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit-report.json&lt;/span&gt;

  &lt;span class="na"&gt;iac&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IaC - Checkov&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret-scan&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Checkov&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install checkov&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Checkov&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov -d terraform/ -o json &amp;gt; checkov-report.json&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov-report.json&lt;/span&gt;

  &lt;span class="na"&gt;container-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Container Scan - Trivy&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;sast&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sca&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;iac&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t devsecops-demo:${{ github.sha }} .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Trivy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devsecops-demo:${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report.json&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CRITICAL,HIGH&lt;/span&gt;
          &lt;span class="na"&gt;exit-code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic is deliberate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Secret scanning runs first. If credentials are found in the code,
nothing else runs. There's no value in scanning code that's already
compromised.&lt;/li&gt;
&lt;li&gt;SAST, SCA, and IaC run in parallel after secrets pass. These are independent checks — no reason to run them sequentially. Running in parallel keeps the pipeline fast.&lt;/li&gt;
&lt;li&gt;Container scanning runs last. It only runs if the three parallel
scans all pass. If the code has known vulnerabilities, there's no point building and scanning the image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What the pipeline run looks like&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmht2pi7a3ktkyfxp4xjx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmht2pi7a3ktkyfxp4xjx.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Secret Scanning passed — no leaks detected. SAST, SCA, and IaC all failed&lt;br&gt;
because the demo app is deliberately broken. Container Scan was skipped.&lt;br&gt;
That skipped Trivy stage is worth explaining. It's not a failure — it's the pipeline working correctly. GitHub Actions skips a job when its dependencies fail. Trivy needed Bandit, pip-audit, and Checkov to all pass before it would run. They didn't, so it didn't. There's no point scanning a container image built from code you already know is vulnerable.&lt;br&gt;
In a real project where the code is clean, all five stages would run and the pipeline would either pass completely or fail at Trivy if the image has CVEs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this pipeline catches&lt;/strong&gt;&lt;br&gt;
Across the five parts of this series, the pipeline found:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Secrets — AWS access keys hardcoded in app.py, caught before commit
and at push time&lt;/li&gt;
&lt;li&gt;Code vulnerabilities — SQL injection, eval() on user input, debug=True in Flask, all flagged by Bandit&lt;/li&gt;
&lt;li&gt;Vulnerable dependencies — 37 known CVEs across 6 packages in requirements.txt, caught by pip-audit&lt;/li&gt;
&lt;li&gt;Infrastructure misconfigurations — 18 failed Checkov checks including
a public S3 bucket, unencrypted EBS, and no IMDSv2 enforcement&lt;/li&gt;
&lt;li&gt;Container CVEs — 1,747 vulnerabilities in the Docker image, 185 of
them CRITICAL, caught by Trivy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this required a security expert. Each tool is open source, free,&lt;br&gt;
and wired into a standard GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this pipeline doesn't catch&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DAST — Dynamic Application Security Testing scans a running application
for vulnerabilities. Nothing here does that. Tools like OWASP ZAP fill
this gap.&lt;/li&gt;
&lt;li&gt;Runtime security — once the container is running in production,
nothing here monitors it. Tools like Falco watch for suspicious behaviour
at runtime.&lt;/li&gt;
&lt;li&gt;Secrets in git history — Gitleaks scans current files and recent
commits. A secret committed years ago and deleted may still be in history.&lt;/li&gt;
&lt;li&gt;Logic flaws — no static analysis tool catches business logic vulnerabilities. Those require manual review.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The repo&lt;/strong&gt;&lt;br&gt;
Everything built across this series is at&lt;br&gt;
&lt;a href="https://github.com/pkkht/devsecops-demo" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo&lt;/a&gt; — the vulnerable Flask app, the Terraform, the Dockerfile, and all the GitHub Actions workflows.&lt;br&gt;
Clone it, run the pipeline, break things deliberately, and see what gets caught.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>github</category>
      <category>security</category>
    </item>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 5 - Container Scanning with Trivy</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 11:00:03 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-5-container-scanning-with-1g6a</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-5-container-scanning-with-1g6a</guid>
      <description>&lt;p&gt;The previous parts secured the code and the infrastructure. This part secures the container image — the thing that actually runs in production.&lt;br&gt;
When you build a Docker image, you're not just shipping your application.&lt;br&gt;
You're shipping the entire base image underneath it — the OS, the system&lt;br&gt;
libraries, the package manager, all of it. Every CVE in those packages is&lt;br&gt;
now your problem.&lt;/p&gt;

&lt;p&gt;Code repo: &lt;a href="https://github.com/pkkht/devsecops-demo/" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What container scanning is&lt;/strong&gt;&lt;br&gt;
Container scanning analyses a built Docker image for known vulnerabilities. It inspects the OS layer, every installed package, and the application dependencies, then cross-references each one against public CVE databases. The key insight: most of the vulnerabilities in a container image come from the base image, not from the application code. Choosing an old or full base image can introduce hundreds of vulnerabilities before you've written a single line of your own code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool: Trivy&lt;/strong&gt;&lt;br&gt;
Trivy is an open source vulnerability scanner from Aqua Security. It scans container images, filesystems, git repositories, Kubernetes clusters, and more. It queries multiple vulnerability databases including the NVD, GitHub Advisory Database, and OS-specific advisories. It's free, fast, and requires no account or API key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The demo Dockerfile&lt;/strong&gt;&lt;br&gt;
The Dockerfile in the repo has two intentional issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ISSUE 1: Using python:3.8 (not slim, not alpine)&lt;/span&gt;
&lt;span class="c"&gt;# An older, full base image with many OS-level packages = more CVE surface.&lt;/span&gt;
&lt;span class="c"&gt;# The fix: use python:3.11-slim or python:3.11-alpine.&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.8&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# ISSUE 2: No USER directive — container runs as root&lt;/span&gt;
&lt;span class="c"&gt;# Running as root means if the app is compromised, the attacker&lt;/span&gt;
&lt;span class="c"&gt;# has root inside the container.&lt;/span&gt;
&lt;span class="c"&gt;# The fix:&lt;/span&gt;
&lt;span class="c"&gt;#   RUN adduser --disabled-password --gecos '' appuser&lt;/span&gt;
&lt;span class="c"&gt;#   USER appuser&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;python:3.8&lt;/strong&gt; is a full Debian-based image. It includes everything — compilers, build tools, image processing libraries, the lot. Most of it is unnecessary for running a Flask API, but all of it adds CVE surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions workflow&lt;/strong&gt;&lt;br&gt;
Create &lt;code&gt;.github/workflows/container-scan.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Container Scan - Trivy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;trivy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Trivy Container Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Docker image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t devsecops-demo:${{ github.sha }} .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Trivy vulnerability scanner&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devsecops-demo:${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
          &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report.json&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CRITICAL,HIGH&lt;/span&gt;
          &lt;span class="na"&gt;exit-code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Trivy Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trivy-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;severity: CRITICAL,HIGH&lt;/code&gt; tells Trivy to only report and gate on CRITICAL and HIGH findings — ignoring MEDIUM and LOW keeps the noise manageable.&lt;br&gt;
exit-code: 1 fails the build when findings are found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the pipeline found&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pipeline failed immediately.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xgb6zcko25pl7hrt5gm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6xgb6zcko25pl7hrt5gm.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trivy-report.json artifact from the Actions run tells the full story.&lt;br&gt;
Total: 1,747 vulnerabilities&lt;br&gt;
OS layer (Debian 12.7):   1,736 vulnerabilities&lt;br&gt;
  CRITICAL: 185&lt;br&gt;
  HIGH:     1,551&lt;/p&gt;

&lt;p&gt;Python packages:          11 vulnerabilities&lt;br&gt;
  HIGH:     11&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OS layer findings&lt;/strong&gt;&lt;br&gt;
The python:3.8 base image ships with ImageMagick, which alone accounts for&lt;br&gt;
185 CRITICAL findings. ImageMagick is an image processing library with a long history of CVEs. You almost certainly don't need it in a Flask API container, but because python:3.8 is a full Debian image, it's there anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python package findings&lt;/strong&gt;&lt;br&gt;
The application dependencies contributed 11 HIGH severity findings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd9bygjmre7cydcu6359.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcd9bygjmre7cydcu6359.png" alt=" " width="612" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These overlap with what &lt;code&gt;pip-audit&lt;/code&gt; found in Part 4 — Trivy is scanning the same packages but from inside the built image rather than from&lt;br&gt;
&lt;strong&gt;requirements.txt&lt;/strong&gt;. Both tools catching the same issues is a good sign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sample finding from the JSON report&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"VulnerabilityID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CVE-2023-30861"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PkgName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"InstalledVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"FixedVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.2.5, 2.3.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Flask vulnerable to possible disclosure of permanent session cookie"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix is simple&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both issues in the Dockerfile have straightforward fixes:&lt;br&gt;
&lt;strong&gt;Switch to a slim base image:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.8&lt;/span&gt;

&lt;span class="c"&gt;# After&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;python:3.11-slim&lt;/code&gt; is a minimal Debian image — no ImageMagick, no compilers, no build tools. The CVE count drops dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a non-root user:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; appuser
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container no longer runs as root.&lt;br&gt;
These two changes would eliminate the vast majority of the 1747 findings.&lt;br&gt;
They are intentionally left in the demo so the pipeline has something real&lt;br&gt;
to catch.&lt;/p&gt;

&lt;p&gt;What we've built so far&lt;br&gt;
Six layers now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitleaks pre-commit — blocks secrets at commit time&lt;/li&gt;
&lt;li&gt;Gitleaks GitHub Actions — catches secrets at push time&lt;/li&gt;
&lt;li&gt;Bandit GitHub Actions — catches code vulnerabilities, gates on HIGH&lt;/li&gt;
&lt;li&gt;pip-audit GitHub Actions — catches vulnerable dependencies&lt;/li&gt;
&lt;li&gt;Checkov GitHub Actions — catches Terraform misconfigurations&lt;/li&gt;
&lt;li&gt;Trivy GitHub Actions — catches CVEs in the container image&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 4 - IaC Scanning with Checkov</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 10:28:06 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-4-iac-scanning-with-kkc</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-4-iac-scanning-with-kkc</guid>
      <description>&lt;p&gt;The previous parts covered application security — secrets, code vulnerabilities, and dependency CVEs. This part shifts to the infrastructure side. The Terraform in the repo describes the AWS resources the app would run on. If that infrastructure is misconfigured, it doesn't matter how clean the application code is. IaC scanning catches those misconfigurations before terraform apply ever runs.&lt;/p&gt;

&lt;p&gt;Code repo: &lt;a href="https://github.com/pkkht/devsecops-demo/" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What IaC scanning is&lt;/strong&gt;&lt;br&gt;
Infrastructure as Code scanning analyses your Terraform, CloudFormation,&lt;br&gt;
Kubernetes manifests, or Helm charts for security misconfigurations. It works the same way as SAST — static analysis, no cloud connection required. It checks your configuration files against a library of security rules and tells you what's wrong and how to fix it.&lt;br&gt;
The value is catching misconfigurations at code review time rather than after they've been deployed to a live environment. An open S3 bucket found in a Terraform file takes seconds to fix. The same bucket found after a data exposure incident is a very different conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool: Checkov&lt;/strong&gt;&lt;br&gt;
Checkov is an open source IaC scanner maintained by Bridgecrew (now part of Palo Alto Networks). It supports Terraform, CloudFormation, Kubernetes, Helm, ARM, Bicep, and more. It maps findings to CWE and CVE identifiers and has over 1000 built-in rules for AWS, Azure, and GCP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The demo Terraform&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The terraform/main.tf in the repo contains 6 intentional misconfigurations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;S3 bucket without server-side encryption — data at rest is unencrypted&lt;/li&gt;
&lt;li&gt;S3 bucket set to public-read — all objects exposed to the internet&lt;/li&gt;
&lt;li&gt;S3 bucket versioning not enabled — no recovery from accidental deletion&lt;/li&gt;
&lt;li&gt;Security group open to 0.0.0.0/0 on all ports — any IP, any port&lt;/li&gt;
&lt;li&gt;EC2 with unencrypted EBS root volume — encrypted = false&lt;/li&gt;
&lt;li&gt;No IMDSv2 enforcement — SSRF attacks can reach the instance metadata service&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local setup is skipped here for brevity — Checkov has known issues running&lt;br&gt;
on Windows paths with spaces. The GitHub Actions workflow handles the scan&lt;br&gt;
in a clean Linux environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions workflow&lt;/strong&gt;&lt;br&gt;
Create &lt;code&gt;.github/workflows/iac.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IaC - Checkov&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkov&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkov IaC Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Checkov&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install checkov&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Checkov&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov -d terraform/ -o json &amp;gt; checkov-report.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Checkov Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkov-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What the pipeline found&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvhqoelghmcxm876zubj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvhqoelghmcxm876zubj.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workflow failed as expected. GitHub Actions downloaded the &lt;code&gt;checkov-report.json&lt;/code&gt; artifact which contains the full findings. Here's a snapshot of what Checkov caught — 18 failed checks across 4 resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"passed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"skipped"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkov_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.2.524"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intentional misconfigurations were all caught, plus several additional&lt;br&gt;
issues Checkov flagged that weren't in the original list. That's worth&lt;br&gt;
noting — a real codebase will always have more findings than you expect.&lt;/p&gt;

&lt;p&gt;Breaking down the failed checks by resource:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws_security_group.app_sg&lt;/code&gt; — 5 failures&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxxgs30r23jrh6hhfo71.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxxgs30r23jrh6hhfo71.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The wide-open ingress rule (&lt;code&gt;from_port = 0, to_port = 65535, cidr = 0.0.0.0/0&lt;/code&gt;) triggered multiple checks because Checkov evaluates each port range independently. One misconfiguration, five findings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws_instance.app_server&lt;/code&gt; — 5 failures&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji099gk0czz9d0us971t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fji099gk0czz9d0us971t.png" alt=" " width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws_s3_bucket.app_storage&lt;/code&gt; — 8 failures&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foo2yctq036ucwwsigvfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foo2yctq036ucwwsigvfg.png" alt=" " width="796" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A single example finding from the JSON report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CKV_AWS_79"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ensure Instance Metadata Service Version 1 is not enabled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"check_result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws_instance.app_server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/main.tf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file_line_range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why 18 failures from 6 misconfigurations&lt;/strong&gt;&lt;br&gt;
One thing worth explaining for readers who run this themselves — you'll see more failures than the 6 intentional misconfigurations. Checkov has over 1000 rules and many of them overlap. A single misconfigured security group rule triggers checks for SSH, RDP, HTTP, and unrestricted egress separately. A single S3 bucket without proper access controls triggers checks for ACLs, public access blocks, KMS encryption, versioning, and logging independently.&lt;br&gt;
This is expected behaviour, not noise. Each check represents a distinct&lt;br&gt;
security concern, and a real security review would assess each one individually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we've built so far&lt;/strong&gt;&lt;br&gt;
Five layers now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitleaks pre-commit — blocks secrets at commit time&lt;/li&gt;
&lt;li&gt;Gitleaks GitHub Actions — catches secrets at push time&lt;/li&gt;
&lt;li&gt;Bandit GitHub Actions — catches code vulnerabilities, gates on HIGH severity&lt;/li&gt;
&lt;li&gt;pip-audit GitHub Actions — catches vulnerable dependencies&lt;/li&gt;
&lt;li&gt;Checkov GitHub Actions — catches Terraform misconfigurations before deployment&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>terraform</category>
    </item>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 3 - SCA with pip-audit</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 09:49:02 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-3-sca-with-pip-audit-5a7l</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-3-sca-with-pip-audit-5a7l</guid>
      <description>&lt;p&gt;Parts 1 and 2 covered the code you write — secrets and static vulnerabilities in &lt;code&gt;app.py&lt;/code&gt;. But modern applications are mostly made up of code you didn't write. Every package in requirements.txt is someone else's code running in your app. If any of those packages have known vulnerabilities, your app inherits them.&lt;br&gt;
That's what SCA is for.&lt;/p&gt;

&lt;p&gt;Code repo: &lt;a href="https://github.com/pkkht/devsecops-demo/" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What SCA is&lt;/strong&gt;&lt;br&gt;
SCA stands for Software Composition Analysis. It looks at your dependency list, checks each package version against public vulnerability databases, and reports any known CVEs. It doesn't analyse your code — it analyses what your code depends on. This matters because a lot of real-world breaches don't come from custom code at all. They come from a vulnerable library that nobody noticed was outdated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tool: pip-audit&lt;/strong&gt;&lt;br&gt;
pip-audit is maintained by the Python Packaging Authority (PyPA) — the same group that maintains pip itself. It queries the Python Packaging Advisory Database (PyPA Advisory DB) and the OSV database for known vulnerabilities. It's free, open source, and requires no account or API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;pip install pip-audit
pip-audit --version
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq5ntnfydtxdlu4prdg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq5ntnfydtxdlu4prdg8.png" alt=" " width="564" height="63"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The demo dependencies&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;requirements.txt&lt;/code&gt; in the repo contains intentionally outdated packages with known CVEs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=1.1.2&lt;/span&gt;
&lt;span class="py"&gt;Jinja2&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.11.3&lt;/span&gt;
&lt;span class="py"&gt;Werkzeug&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=1.0.1&lt;/span&gt;
&lt;span class="py"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.20.0&lt;/span&gt;
&lt;span class="py"&gt;itsdangerous&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=1.1.0&lt;/span&gt;
&lt;span class="py"&gt;SQLAlchemy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=1.3.20&lt;/span&gt;
&lt;span class="py"&gt;click&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=7.1.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are real versions that were in production use a few years ago. A lot of projects still have dependency files that haven't been updated in that kind of timeframe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running pip-audit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;pip-audit -r requirements.txt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgg7f0u5bfelr1qej1t9b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgg7f0u5bfelr1qej1t9b.png" alt=" " width="800" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;37 known vulnerabilities across 6 packages. That's from 7 packages in&lt;br&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; — only one came back clean. Some of the findings worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flask 1.1.2 — 2 vulnerabilities, fixed in 2.2.5&lt;/li&gt;
&lt;li&gt;Jinja2 2.11.3 — 4 vulnerabilities including CVE-2024-22195 and CVE-2024-34064, fixed in 3.1.3+&lt;/li&gt;
&lt;li&gt;Werkzeug 1.0.1 — 13 vulnerabilities, the most of any package, fixed versions ranging up to 3.1.6&lt;/li&gt;
&lt;li&gt;requests 2.20.0 — 5 vulnerabilities including CVE-2024-35195 and CVE-2026-25645, fixed in 2.31.0+&lt;/li&gt;
&lt;li&gt;urllib3 — 13 vulnerabilities flagged as a transitive dependency (pulled in by requests)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix version column tells you exactly what to upgrade to. That's the&lt;br&gt;
output you hand to a developer — not a vague warning, but a specific action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a JSON report&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;pip-audit -r requirements.txt -f json -o pip-audit-report.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa5hxngbnhls57bcmipf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa5hxngbnhls57bcmipf.png" alt=" " width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same findings, machine-readable output. Add it to &lt;code&gt;.gitignore&lt;/code&gt; so it doesn't get committed to the repo:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mu8zn8nrqhd8jd36pif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4mu8zn8nrqhd8jd36pif.png" alt=" " width="466" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/sca.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SCA - pip-audit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pip-audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit SCA Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install pip-audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install pip-audit&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run pip-audit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit -r requirements.txt -f json -o pip-audit-report.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload pip-audit Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip-audit-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push and watch it run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj3eo24to5q6r7bu5zae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuj3eo24to5q6r7bu5zae.png" alt=" " width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pipeline fails at the &lt;strong&gt;Run pip-audit&lt;/strong&gt; step — "Found 37 known&lt;br&gt;
vulnerabilities in 6 packages", exit code 1. The report is still uploaded&lt;br&gt;
as an artifact via the &lt;code&gt;if: always()&lt;/code&gt; step so the findings are available even though the build failed.&lt;br&gt;
Again — this is the correct behaviour. The pipeline found real vulnerabilities and stopped the build. In a real project the fix is straightforward: update the packages to the versions shown in the Fix Versions column, push again, and the build passes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we've built so far&lt;/strong&gt;&lt;br&gt;
Four layers now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitleaks pre-commit — blocks secrets at commit time&lt;/li&gt;
&lt;li&gt;Gitleaks GitHub Actions — catches secrets at push time&lt;/li&gt;
&lt;li&gt;Bandit GitHub Actions — catches code vulnerabilities, gates on HIGH severity&lt;/li&gt;
&lt;li&gt;pip-audit GitHub Actions — catches vulnerable dependencies&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>python</category>
      <category>security</category>
      <category>tooling</category>
    </item>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 2 - SAST with Bandit</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 09:32:22 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-2-sast-with-bandit-3d1d</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-2-sast-with-bandit-3d1d</guid>
      <description>&lt;p&gt;Part 1 covered secret scanning with Gitleaks — catching credentials before they reach the repo. That's one layer. But credentials aren't the only problem in &lt;code&gt;app.py&lt;/code&gt;. There's a SQL injection vulnerability, an &lt;code&gt;eval()&lt;/code&gt; call that lets an attacker run arbitrary Python code, and debug mode left on. None of those are secrets. Gitleaks won't touch them.&lt;br&gt;
That's what SAST is for.&lt;/p&gt;

&lt;p&gt;Code repo: &lt;a href="https://github.com/pkkht/devsecops-demo/" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What SAST is
&lt;/h2&gt;

&lt;p&gt;SAST stands for Static Application Security Testing. It analyses your source code without running it, looking for patterns that indicate security vulnerabilities. No server needed, no database, no HTTP requests — just the code itself.&lt;br&gt;
The key difference from a linter: SAST is specifically looking for security issues, not style or correctness. It knows what SQL injection looks like. It knows which Python functions are dangerous. It knows that &lt;code&gt;debug=True&lt;/code&gt; in a Flask app exposes the Werkzeug interactive debugger to anyone who can reach it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tool: Bandit
&lt;/h2&gt;

&lt;p&gt;Bandit is the standard SAST tool for Python.&lt;br&gt;
It is open source, maintained by the Python Security community, and maps its findings to CWE (Common Weakness Enumeration) IDs so you know exactly what class of vulnerability you're dealing with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing Bandit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;bandit
bandit &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7yof4fbdvkc06kt1i3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs7yof4fbdvkc06kt1i3d.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it against the app&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bandit &lt;span class="nt"&gt;-r&lt;/span&gt; app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; flag means recursive — scan the directory, not just a single file. Here it's running against &lt;code&gt;app.py&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuc8qogzwsmm3n5jjgqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwuc8qogzwsmm3n5jjgqx.png" alt=" " width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first findings are hardcoded passwords — supersecretkey123, the API token, the AWS keys. These are all flagged as B105: hardcoded_password_string, severity Low. Bandit and Gitleaks overlap here — both tools catch hardcoded credentials, just from different angles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dzzxt7ri7vn79e4emuy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3dzzxt7ri7vn79e4emuy.png" alt=" " width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The more serious findings:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B608: hardcoded_sql_expressions&lt;/strong&gt; — the f-string SQL query on line 69. Severity Medium. This is the SQL injection vulnerability — user input is&lt;br&gt;
embedded directly into the query string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B307: blacklist&lt;/strong&gt; — the &lt;code&gt;eval()&lt;/code&gt; call on line 127. Severity Medium,&lt;br&gt;
Confidence High. Bandit flags &lt;code&gt;eval()&lt;/code&gt; as blacklisted because it executes&lt;br&gt;
arbitrary code. An attacker who can reach the &lt;code&gt;/calculate&lt;/code&gt; endpoint can run anything on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B201: flask_debug_true&lt;/strong&gt; — &lt;code&gt;debug=True&lt;/code&gt; on line 137. Severity High.&lt;br&gt;
The Werkzeug debugger is interactive — if an unhandled exception hits in&lt;br&gt;
production, anyone who sees the error page gets a Python shell.&lt;/p&gt;

&lt;p&gt;B104: hardcoded_bind_all_interfaces — &lt;code&gt;host="0.0.0.0"&lt;/code&gt; on line 137.&lt;br&gt;
Severity Medium. The app is listening on every network interface, not just&lt;br&gt;
localhost.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs63r7qc2u3t3xxolbbyf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs63r7qc2u3t3xxolbbyf.png" alt=" " width="729" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The summary: 81 lines of code, 8 issues total — 4 Low, 3 Medium, 1 High.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filtering by severity&lt;/strong&gt;&lt;br&gt;
In a real pipeline you don't want to fail on every Low finding — you'd never ship anything. The practical approach is to gate on High severity only, and report everything else for visibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bandit &lt;span class="nt"&gt;-r&lt;/span&gt; app.py &lt;span class="nt"&gt;--severity-level&lt;/span&gt; high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F997ez2mqm1r7fkbc23ml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F997ez2mqm1r7fkbc23ml.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;--severity-level&lt;/code&gt; high, only one finding comes through: the Flask&lt;br&gt;
&lt;code&gt;debug=True&lt;/code&gt;. That's the gate. Everything else is still visible in the full report but won't block the build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a JSON report&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bandit &lt;span class="nt"&gt;-r&lt;/span&gt; app.py &lt;span class="nt"&gt;-f&lt;/span&gt; json &lt;span class="nt"&gt;-o&lt;/span&gt; bandit-report.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75lidqjrrbcd5dyly0t0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75lidqjrrbcd5dyly0t0.png" alt=" " width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The JSON output is what the pipeline uses — it's machine-readable and can be uploaded as a build artifact. One thing to watch: the report contains the actual secret values from the code as context snippets. Add it to .gitignore so it doesn't get committed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48em3mevb5pieu69zknr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F48em3mevb5pieu69zknr.png" alt=" " width="323" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions workflow&lt;/strong&gt;&lt;br&gt;
Create &lt;code&gt;.github/workflows/sast.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SAST - Bandit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;bandit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bandit SAST Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.11'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Bandit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install bandit&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Bandit&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit -r app.py --severity-level high -f json -o bandit-report.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Bandit Report&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bandit-report.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;if: always()&lt;/code&gt; on the upload step is important — it means the report gets&lt;br&gt;
uploaded even when the scan fails, so you can inspect the findings.&lt;br&gt;
Push it and watch it run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xatuhe6jmsxa7jb8xme.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0xatuhe6jmsxa7jb8xme.png" alt=" " width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pipeline fails. This is the right outcome — Bandit found a HIGH severity issue (debug=True) and exited with code 1. The bandit-report artifact is still uploaded and available for download.&lt;br&gt;
This is the pipeline doing its job. In a real codebase, a developer would fix &lt;code&gt;debug=True&lt;/code&gt;, push again, and the build would pass. In this demo repo the vulnerability is intentional, so we leave it failing as a demonstration that the gate is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we've built so far&lt;/strong&gt;&lt;br&gt;
Three layers are now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gitleaks pre-commit hook — blocks secrets at commit time&lt;/li&gt;
&lt;li&gt;Gitleaks GitHub Actions — catches secrets at push time&lt;/li&gt;
&lt;li&gt;Bandit GitHub Actions — catches code vulnerabilities at push time, gates on HIGH severity&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>python</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>DevSecOps in Practice: Tools That Actually Catch Vulnerabilities - Part 1 - Secret Scanning with Gitleaks</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Sun, 26 Apr 2026 08:48:08 +0000</pubDate>
      <link>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-1-43og</link>
      <guid>https://dev.to/pkkht/devsecops-in-practice-tools-that-actually-catch-vulnerabilities-part-1-43og</guid>
      <description>&lt;h2&gt;
  
  
  Secret Scanning with Gitleaks
&lt;/h2&gt;

&lt;p&gt;I have built a deliberately vulnerable Flask app to use as a target for building a real DevSecOps pipeline. The repo is at&lt;br&gt;
&lt;a href="https://github.com/pkkht/devsecops-demo" rel="noopener noreferrer"&gt;https://github.com/pkkht/devsecops-demo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This part covers the first gate in the pipeline — secret scanning.&lt;/p&gt;

&lt;p&gt;Why secrets in code are such a big deal?&lt;br&gt;
AWS access keys, API tokens, database passwords — they end up in source code more often than you would think. A developer hardcodes a key to test something locally, forgets to remove it, and commits it. If the repo is public even for a minute, bots are scanning GitHub continuously and will find it.&lt;br&gt;
It is one of the most preventable attack vectors and one of the most common. The fix is to catch it before the commit happens.&lt;/p&gt;

&lt;p&gt;The demo app already has secrets in it - intentionally added.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpaavq6qen7di3dtw1ki3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpaavq6qen7di3dtw1ki3.png" alt=" " width="783" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will use Gitleaks to catch the exposed secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Gitleaks&lt;/strong&gt;&lt;br&gt;
Gitleaks is an open source secret scanner. It scans your code and git history for secrets using a set of regex rules covering AWS keys, GitHub tokens, private keys, generic API keys, and more. It is free, fast, and has no dependencies on any cloud service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting it up locally with pre-commit&lt;/strong&gt;&lt;br&gt;
The goal is to stop secrets at the developer's machine — before they even&lt;br&gt;
become a commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Install pre-commit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pre-commit is a framework for managing git hooks. It handles downloading and running Gitleaks automatically — you do not need to install Gitleaks separately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pre-commit
pre-commit &lt;span class="nt"&gt;--version&lt;/span&gt;
pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command wires Gitleaks into your &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; so it runs automatically on every &lt;code&gt;git commit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkntca2zodoejn7n2prbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkntca2zodoejn7n2prbi.png" alt=" " width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create the config file&lt;/strong&gt;&lt;br&gt;
Create &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; in your repo root:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfdll18mwtrp23jdu5kf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfdll18mwtrp23jdu5kf.png" alt=" " width="626" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seeing it in action&lt;/strong&gt;&lt;br&gt;
Let's try to commit &lt;code&gt;app.py&lt;/code&gt; with the secrets still in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add app.py
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add task manager app"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First attempt — before the config file was in place:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuqqzikdm1bv5uchn6z8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuqqzikdm1bv5uchn6z8.png" alt=" " width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pre-commit requires a &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; to be present. Once that is added, try again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xdmpvhqi8ll1lmf43b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9xdmpvhqi8ll1lmf43b4.png" alt=" " width="800" height="693"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The commit is blocked. Gitleaks downloads itself, scans the staged files, and reports two findings — the AWS access key and the secret key — with the exact file and line number. The commit never happens. This is the behaviour you want. A developer cannot accidentally push a secret&lt;br&gt;
without actively bypassing the hook.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling intentional secrets in a demo repo
&lt;/h2&gt;

&lt;p&gt;In a real codebase, a finding like this means: remove the secret, rotate it, use an environment variable instead. But this is a demo repo — the secrets are intentional. We need a way to tell Gitleaks "I know about these, ignore them."&lt;br&gt;
That's what &lt;code&gt;.gitleaksignore&lt;/code&gt; is for. You add the fingerprints of known,&lt;br&gt;
intentional findings:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcyk6oyeqozt6opn6ries.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcyk6oyeqozt6opn6ries.png" alt=" " width="465" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fingerprints come directly from the Gitleaks output and an additional line that you may wish to add — file:rule:line.&lt;br&gt;
Once added, the commit goes through:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrjsh8p8q7pwkoqwkvmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmrjsh8p8q7pwkoqwkvmu.png" alt=" " width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Detect hardcoded secrets... Passed"&lt;/em&gt;&lt;br&gt;
In a real project, &lt;code&gt;.gitleaksignore&lt;/code&gt; is useful for allowlisting known false&lt;br&gt;
positives — test fixtures, example keys in documentation. It is not a way to skip real secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wiring it into GitHub Actions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pre-commit hook covers local commits. But what if someone bypasses it with &lt;code&gt;--no-verify&lt;/code&gt;, or clones the repo without setting up pre-commit? The pipeline is the safety net.&lt;br&gt;
Create &lt;code&gt;.github/workflows/secret-scan.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret Scanning&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gitleaks Secret Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Gitleaks&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetch-depth: 0&lt;/code&gt; is important — it tells GitHub Actions to check out the full git history, not just the latest commit. Gitleaks needs the full history to scan previous commits for secrets.&lt;br&gt;
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is automatically provided by GitHub on every workflow run —&lt;br&gt;
you don't need to create it. Gitleaks uses it to post findings as job summaries.&lt;br&gt;
Push the workflow file and watch it run in the Actions tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5plero54wdjxn74z01md.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5plero54wdjxn74z01md.png" alt=" " width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"No leaks detected" — the pipeline passes because the &lt;code&gt;.gitleaksignore&lt;/code&gt;&lt;br&gt;
covers the intentional secrets in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we've built so far
&lt;/h2&gt;

&lt;p&gt;Two layers of secret scanning are now in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-commit hook — catches secrets at the developer's machine before commit&lt;/li&gt;
&lt;li&gt;GitHub Actions workflow — catches anything that slips through at push time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline now has its first gate. Every push and pull request is&lt;br&gt;
automatically scanned for secrets.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My experience in passing HashiCorp Certified: Terraform Associate (003) exam</title>
      <dc:creator>Hariharan</dc:creator>
      <pubDate>Wed, 01 Nov 2023 18:48:56 +0000</pubDate>
      <link>https://dev.to/pkkht/my-experience-in-passing-hashicorp-certified-terraform-associate-003-exam-15a5</link>
      <guid>https://dev.to/pkkht/my-experience-in-passing-hashicorp-certified-terraform-associate-003-exam-15a5</guid>
      <description>&lt;p&gt;Straight to the subject point:&lt;/p&gt;

&lt;p&gt;It took me less than 2 months to prepare for the exam and I attended the exam on Nov 1, 2023. I passed with the score of 93%.&lt;br&gt;
I thought I would share my exam preparation experience.&lt;/p&gt;

&lt;p&gt;Firstly, I started reading this book "Terraform: Up and Running" by Brikman.&lt;br&gt;
&lt;a href="https://learning.oreilly.com/library/view/terraform-up/9781492046899/" rel="noopener noreferrer"&gt;https://learning.oreilly.com/library/view/terraform-up/9781492046899/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This book gave me a good understanding of IaC and Terraform workflow. I strongly recommend reading the first 5 chapters in the book for the exam. And don't forget to practise the Terraform coding whilst reading! I used AWS cloud to provision resources as mentioned in the book. (It doesn't matter which cloud provider you choose.)&lt;/p&gt;

&lt;p&gt;HashiCorp has good documentation of the Terraform product and I used that as a reference too.&lt;/p&gt;

&lt;p&gt;Once I got confidence with the basics, I started doing the practice tests that are available in Udemy and O'Reilly. I had 80% as a benchmark in passing these exams. The tests available in these sites come with detailed explanations for the answers.&lt;/p&gt;

&lt;p&gt;That is it! Good luck with your exam preparation!!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>certification</category>
    </item>
  </channel>
</rss>
