<?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: Nikolay Kuziev</title>
    <description>The latest articles on DEV Community by Nikolay Kuziev (@nkuziev-sec).</description>
    <link>https://dev.to/nkuziev-sec</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%2F3917012%2F88d37d37-869a-40bc-b391-c2a104a830f0.png</url>
      <title>DEV Community: Nikolay Kuziev</title>
      <link>https://dev.to/nkuziev-sec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nkuziev-sec"/>
    <language>en</language>
    <item>
      <title>Why Fixed Container Image Versions Matter: Lessons from the Trivy Supply Chain Attack</title>
      <dc:creator>Nikolay Kuziev</dc:creator>
      <pubDate>Thu, 07 May 2026 05:39:20 +0000</pubDate>
      <link>https://dev.to/nkuziev-sec/why-fixed-container-image-versions-matter-lessons-from-the-trivy-supply-chain-attack-29ke</link>
      <guid>https://dev.to/nkuziev-sec/why-fixed-container-image-versions-matter-lessons-from-the-trivy-supply-chain-attack-29ke</guid>
      <description>&lt;p&gt;A CI/CD pipeline is only as trustworthy as the code and tools it pulls during execution.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it is easy to forget.&lt;/p&gt;

&lt;p&gt;Most supply chain conversations start with application dependencies: Maven artifacts, Gradle dependencies, npm packages, base images, operating system packages, and third-party libraries.&lt;/p&gt;

&lt;p&gt;But CI/CD tools are dependencies too.&lt;/p&gt;

&lt;p&gt;The image used by a pipeline job is a dependency. The scanner image used in a security stage is a dependency. The GitHub Action used to scan containers is a dependency. The script downloaded with &lt;code&gt;curl | sh&lt;/code&gt; is a dependency. The &lt;code&gt;latest&lt;/code&gt; tag is also a dependency, but one that can change without a pull request.&lt;/p&gt;

&lt;p&gt;That is the part I want to focus on here.&lt;/p&gt;

&lt;p&gt;The Trivy supply chain incident is a useful case study because Trivy is not a random tool. It is a widely used security scanner. Many teams run it in CI/CD specifically to improve supply chain security.&lt;/p&gt;

&lt;p&gt;That is the uncomfortable lesson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a security scanner can also become a supply chain dependency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not mean "do not use Trivy".&lt;/p&gt;

&lt;p&gt;It does not mean "security scanners are bad".&lt;/p&gt;

&lt;p&gt;It means we need to treat CI/CD tooling with the same discipline we expect from application dependencies.&lt;/p&gt;

&lt;p&gt;The minimum practical step is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;do not use floating image tags for security-critical CI/CD tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use fixed versions. For stronger integrity, use image digests. For GitHub Actions, use full commit SHAs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened with Trivy
&lt;/h2&gt;

&lt;p&gt;In March 2026, Aqua Security published an advisory for a Trivy ecosystem supply chain compromise.&lt;/p&gt;

&lt;p&gt;According to the &lt;a href="https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" rel="noopener noreferrer"&gt;Aqua Security advisory&lt;/a&gt;, on March 19, 2026, a threat actor used compromised credentials to publish a malicious Trivy &lt;code&gt;v0.69.4&lt;/code&gt; release, force-push most &lt;code&gt;aquasecurity/trivy-action&lt;/code&gt; version tags to credential-stealing malware, and replace &lt;code&gt;aquasecurity/setup-trivy&lt;/code&gt; tags with malicious commits.&lt;/p&gt;

&lt;p&gt;The same advisory says that on March 22, 2026, malicious Docker Hub images were published for Trivy &lt;code&gt;v0.69.5&lt;/code&gt; and &lt;code&gt;v0.69.6&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Docker's write-up adds the image-consumer view. Docker reported that Docker Hub customers may have been affected if they pulled &lt;code&gt;aquasec/trivy&lt;/code&gt; with tags &lt;code&gt;0.69.4&lt;/code&gt;, &lt;code&gt;0.69.5&lt;/code&gt;, &lt;code&gt;0.69.6&lt;/code&gt;, or &lt;code&gt;latest&lt;/code&gt; during the affected window between March 19 and March 23, 2026. Docker also described the &lt;code&gt;latest&lt;/code&gt; tag being re-pointed during the incident.&lt;/p&gt;

&lt;p&gt;That is exactly why mutable tags matter.&lt;/p&gt;

&lt;p&gt;If a pipeline used this:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then the pipeline was not really saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use the Trivy version I reviewed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use whatever the latest tag points to at runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference is small in YAML and huge in security.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not only latest
&lt;/h2&gt;

&lt;p&gt;The obvious bad example is &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the deeper problem is mutability.&lt;/p&gt;

&lt;p&gt;Docker's own build best practices explain that image tags are mutable: a publisher can update a tag so it points to a different image later. Docker also notes the downside for consumers: the same build can use different image content over time, and the exact version used becomes harder to audit.&lt;/p&gt;

&lt;p&gt;So this is risky:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this is not perfect either:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:0.69.3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is better because the intended version is visible. It narrows the update surface. It makes the pipeline more readable.&lt;/p&gt;

&lt;p&gt;But it is still a tag.&lt;/p&gt;

&lt;p&gt;A tag can move.&lt;/p&gt;

&lt;p&gt;The stronger form is:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:0.69.3@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag keeps the file human-readable. The digest pins the image content.&lt;/p&gt;

&lt;p&gt;That is the practical difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tag:
  human-readable pointer that can change

digest:
  content identifier for a specific image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For security-sensitive CI/CD jobs, that difference matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task: controlled updates instead of silent updates
&lt;/h2&gt;

&lt;p&gt;The task is not "never update images".&lt;/p&gt;

&lt;p&gt;That would create a different security problem.&lt;/p&gt;

&lt;p&gt;The task is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use controlled updates instead of silent updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want CI/CD tools to change through reviewable pull requests, not by silently pulling a changed tag during a pipeline run.&lt;/p&gt;

&lt;p&gt;That applies to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI job images;&lt;/li&gt;
&lt;li&gt;scanner images;&lt;/li&gt;
&lt;li&gt;Dockerfile base images;&lt;/li&gt;
&lt;li&gt;GitHub Actions;&lt;/li&gt;
&lt;li&gt;Kubernetes workload images;&lt;/li&gt;
&lt;li&gt;sidecar containers;&lt;/li&gt;
&lt;li&gt;init containers;&lt;/li&gt;
&lt;li&gt;build containers;&lt;/li&gt;
&lt;li&gt;release tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to make toolchain changes visible.&lt;/p&gt;

&lt;p&gt;If Trivy changes from one version to another, I want that change to appear in Git history. If a digest changes, I want a pull request. If a scanner is upgraded, I want the pipeline diff to show what changed.&lt;/p&gt;

&lt;p&gt;Not because every update must be slow.&lt;/p&gt;

&lt;p&gt;Because invisible updates are hard to audit during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI/CD images are high-trust dependencies
&lt;/h2&gt;

&lt;p&gt;A scanner image running in CI/CD is not just another container.&lt;/p&gt;

&lt;p&gt;It often has access to the repository checkout. It may read dependency files, Dockerfiles, Kubernetes manifests, lockfiles, SBOMs, source code, and build outputs.&lt;/p&gt;

&lt;p&gt;Depending on the pipeline design, it may also run in an environment where secrets exist.&lt;/p&gt;

&lt;p&gt;GitHub's security guidance warns that a compromised action inside a workflow can be significant because actions may access repository secrets and use the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt;. The same trust model applies to containerized CI tools.&lt;/p&gt;

&lt;p&gt;If a CI job pulls a scanner image and runs it inside the pipeline, that image becomes part of the trusted execution path.&lt;/p&gt;

&lt;p&gt;That is why I do not like treating CI tool versions casually.&lt;/p&gt;

&lt;p&gt;A scanner is still code.&lt;/p&gt;

&lt;p&gt;A scanner image is still a dependency.&lt;/p&gt;

&lt;p&gt;A scanner action is still third-party execution inside a trusted pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bad pattern
&lt;/h2&gt;

&lt;p&gt;A common GitLab CI job might look like this:&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;trivy:image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:latest&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simple, but it has two problems.&lt;/p&gt;

&lt;p&gt;First, the version is not controlled.&lt;/p&gt;

&lt;p&gt;The pipeline may use different Trivy image content tomorrow without any change in the repository.&lt;/p&gt;

&lt;p&gt;Second, incident response becomes harder.&lt;/p&gt;

&lt;p&gt;If someone asks this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Which exact Trivy image did this pipeline use last Tuesday?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the Git repository alone cannot answer it. The team may need registry logs, runner logs, caches, timestamps, and a lot of luck.&lt;/p&gt;

&lt;p&gt;That is a weak audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better pattern: fixed version
&lt;/h2&gt;

&lt;p&gt;A better first step is to use a fixed version tag:&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;trivy:image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:0.69.3&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is already better than &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It makes the intended version visible in Git. It reduces surprise. It makes updates explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- image: aquasec/trivy:0.69.3
&lt;/span&gt;&lt;span class="gi"&gt;+ image: aquasec/trivy:0.70.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For many teams, fixed tags are a practical starting point. They are readable and easy to adopt.&lt;/p&gt;

&lt;p&gt;But for security-critical CI/CD tools, I would treat fixed tags as the minimum, not the end state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stronger pattern: version plus digest
&lt;/h2&gt;

&lt;p&gt;The stronger pattern is to use both a readable version and a digest:&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;trivy:image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:0.69.3@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives two benefits.&lt;/p&gt;

&lt;p&gt;The tag keeps the file readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aquasec/trivy:0.69.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The digest pins the content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sha256:&amp;lt;pinned-image-digest&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the repository says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use this exact image content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the tag moves later, the digest still points to the committed image content.&lt;/p&gt;

&lt;p&gt;Docker's documentation makes the same point for base images: pinning an image to a digest helps ensure the same image is used even if the publisher updates the tag. Docker also notes the tradeoff: pinned digests require an update workflow so teams do not miss security fixes.&lt;/p&gt;

&lt;p&gt;That tradeoff is real.&lt;/p&gt;

&lt;p&gt;Digest pinning gives control, but it also creates responsibility.&lt;/p&gt;

&lt;p&gt;You need a process to update pins.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Trivy incident teaches here
&lt;/h2&gt;

&lt;p&gt;The Aqua advisory explicitly says users were not affected if they used Trivy images referenced by digest.&lt;/p&gt;

&lt;p&gt;That does not mean digest pinning solves every possible supply chain attack.&lt;/p&gt;

&lt;p&gt;It means digest pinning changes the failure mode.&lt;/p&gt;

&lt;p&gt;With a floating tag, your pipeline may silently start using newly pushed content.&lt;/p&gt;

&lt;p&gt;With a pinned digest, your pipeline keeps using the exact content that was reviewed and committed.&lt;/p&gt;

&lt;p&gt;That is a huge difference during incident response.&lt;/p&gt;

&lt;p&gt;When something like this happens, teams need answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did we pull the affected image?&lt;/li&gt;
&lt;li&gt;Which pipelines used it?&lt;/li&gt;
&lt;li&gt;Which version did we use?&lt;/li&gt;
&lt;li&gt;Which digest did we use?&lt;/li&gt;
&lt;li&gt;Which secrets existed in those jobs?&lt;/li&gt;
&lt;li&gt;Which runners executed those jobs?&lt;/li&gt;
&lt;li&gt;Do we need to rotate credentials?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pinned versions and digests make those questions easier to answer.&lt;/p&gt;

&lt;p&gt;Floating tags make them harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  But pinned images get old
&lt;/h2&gt;

&lt;p&gt;This is the strongest argument against pinning.&lt;/p&gt;

&lt;p&gt;And it is not wrong.&lt;/p&gt;

&lt;p&gt;If a team pins an image and forgets about it for two years, that is also bad.&lt;/p&gt;

&lt;p&gt;Pinning is not supposed to mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;never update this dependency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;do not update this dependency invisibly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a big difference.&lt;/p&gt;

&lt;p&gt;The workflow should look more like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pin image
scan image
run pipeline
create automated update pull requests
review digest changes
merge updates regularly
keep the audit trail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker's docs describe the same tradeoff: digest pinning improves reproducibility, but teams should have tooling that detects outdated pins and raises pull requests for controlled updates.&lt;/p&gt;

&lt;p&gt;So the real rule is not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pin and forget
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pin and update intentionally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI/CD should not change without Git history
&lt;/h2&gt;

&lt;p&gt;This is the main engineering point for me.&lt;/p&gt;

&lt;p&gt;A pipeline should be reproducible enough that I can look at a commit and understand the tools that ran for that commit.&lt;/p&gt;

&lt;p&gt;If the pipeline uses:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then the same commit can run with different Trivy contents on different days.&lt;/p&gt;

&lt;p&gt;That is not reproducible.&lt;/p&gt;

&lt;p&gt;If the pipeline uses:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasec/trivy:0.69.3@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then the tool is part of the commit state.&lt;/p&gt;

&lt;p&gt;A change requires a diff. A diff can be reviewed. A review can be audited.&lt;/p&gt;

&lt;p&gt;That is what I want from security tooling.&lt;/p&gt;

&lt;p&gt;Not because every update needs a meeting.&lt;/p&gt;

&lt;p&gt;Because CI/CD is part of the software supply chain, and supply chain changes should leave a trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfile base images have the same problem
&lt;/h2&gt;

&lt;p&gt;This is not only about scanner images.&lt;/p&gt;

&lt;p&gt;Base images have the same issue.&lt;/p&gt;

&lt;p&gt;A Dockerfile like this is convenient:&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;FROM&lt;/span&gt;&lt;span class="s"&gt; eclipse-temurin:17&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;eclipse-temurin:17&lt;/code&gt; is still a moving tag.&lt;/p&gt;

&lt;p&gt;A more controlled version is:&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;FROM&lt;/span&gt;&lt;span class="s"&gt; eclipse-temurin:17.0.11_9-jre&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stronger version is:&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;FROM&lt;/span&gt;&lt;span class="s"&gt; eclipse-temurin:17.0.11_9-jre@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea applies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;floating major tag:
  easy, but changes silently

specific version tag:
  better, but still mutable

version plus digest:
  stronger and more reproducible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production images, I prefer the third form when the team has automation to update it.&lt;/p&gt;

&lt;p&gt;Without update automation, teams often resist digests because they feel manual and annoying.&lt;/p&gt;

&lt;p&gt;That is fair.&lt;/p&gt;

&lt;p&gt;The solution is not to abandon digest pinning.&lt;/p&gt;

&lt;p&gt;The solution is to automate digest updates through reviewable pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions need SHA pinning
&lt;/h2&gt;

&lt;p&gt;Container images are pinned by digest.&lt;/p&gt;

&lt;p&gt;GitHub Actions are pinned by full commit SHA.&lt;/p&gt;

&lt;p&gt;This is the same principle in another format.&lt;/p&gt;

&lt;p&gt;Weak:&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="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;aquasecurity/trivy-action@master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&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="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;aquasecurity/trivy-action@0.35.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stronger:&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="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;aquasecurity/trivy-action@&amp;lt;full-commit-sha&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub's own documentation says pinning an action to a full-length commit SHA is the only way to use an action as an immutable release. GitHub also provides repository and organization policies that can require full SHA pinning.&lt;/p&gt;

&lt;p&gt;This matters because the Trivy incident affected both container images and GitHub Actions.&lt;/p&gt;

&lt;p&gt;So the policy should cover both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container image:
  pin by digest

GitHub Action:
  pin by full commit SHA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I would standardize in CI/CD
&lt;/h2&gt;

&lt;p&gt;If I were standardizing this across repositories, I would start with a small policy.&lt;/p&gt;

&lt;p&gt;Not a huge platform rewrite.&lt;/p&gt;

&lt;p&gt;Just a clear baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no &lt;code&gt;:latest&lt;/code&gt; in CI/CD jobs;&lt;/li&gt;
&lt;li&gt;security scanner images must use fixed version tags;&lt;/li&gt;
&lt;li&gt;high-trust CI/CD tooling should be pinned by digest;&lt;/li&gt;
&lt;li&gt;GitHub Actions should be pinned by full commit SHA;&lt;/li&gt;
&lt;li&gt;Dockerfile base images should use specific versions and preferably digests;&lt;/li&gt;
&lt;li&gt;updates should happen through pull requests, not silent tag movement;&lt;/li&gt;
&lt;li&gt;CI/CD should fail when new floating tags are introduced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is small enough to adopt.&lt;/p&gt;

&lt;p&gt;But it changes the trust model.&lt;/p&gt;

&lt;p&gt;The pipeline stops saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull whatever this tag means today
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and starts saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use this reviewed version of this tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: GitLab CI with pinned Trivy image
&lt;/h2&gt;

&lt;p&gt;A simple version:&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;TRIVY_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aquasec/trivy:0.69.3"&lt;/span&gt;

&lt;span class="na"&gt;trivy:image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$TRIVY_IMAGE"&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stronger version:&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;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;TRIVY_IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aquasec/trivy:0.69.3@sha256:&amp;lt;pinned-image-digest&amp;gt;"&lt;/span&gt;

&lt;span class="na"&gt;trivy:image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$TRIVY_IMAGE"&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;trivy image "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I prefer keeping the image reference in a variable.&lt;/p&gt;

&lt;p&gt;That makes updates easier to review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; variables:
&lt;span class="gd"&gt;-  TRIVY_IMAGE: "aquasec/trivy:0.69.3@sha256:&amp;lt;old-digest&amp;gt;"
&lt;/span&gt;&lt;span class="gi"&gt;+  TRIVY_IMAGE: "aquasec/trivy:0.70.0@sha256:&amp;lt;new-digest&amp;gt;"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The diff clearly shows that the scanner changed.&lt;/p&gt;

&lt;p&gt;That is exactly what I want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Docker run with digest
&lt;/h2&gt;

&lt;p&gt;Some projects run Trivy through Docker directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
  aquasec/trivy:latest &lt;span class="se"&gt;\&lt;/span&gt;
  image my-service:local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would avoid this in CI.&lt;/p&gt;

&lt;p&gt;A better version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
  aquasec/trivy:0.69.3@sha256:&amp;lt;pinned-image-digest&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  image my-service:local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the exact command.&lt;/p&gt;

&lt;p&gt;The important part is that the tool image is not resolved through a moving tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: Kubernetes workloads
&lt;/h2&gt;

&lt;p&gt;The same problem exists in Kubernetes manifests.&lt;/p&gt;

&lt;p&gt;Weak:&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;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/payment-api:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&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;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/payment-api:1.14.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stronger:&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;containers&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.example.com/payment-api:1.14.2@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production Kubernetes workloads, I would also want policy enforcement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reject &lt;code&gt;:latest&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;require image digests in production namespaces;&lt;/li&gt;
&lt;li&gt;allow version tags in development namespaces;&lt;/li&gt;
&lt;li&gt;require images from approved registries;&lt;/li&gt;
&lt;li&gt;require signed images for critical workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Kyverno, OPA/Gatekeeper, admission controllers, and registry policies become useful.&lt;/p&gt;

&lt;p&gt;But the first step is still cultural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stop treating image tags as immutable versions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to detect the bad pattern
&lt;/h2&gt;

&lt;p&gt;A simple first check can be basic.&lt;/p&gt;

&lt;p&gt;For GitLab CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"image: .*:latest"&lt;/span&gt; .gitlab-ci.yml .gitlab-ci/ &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Dockerfiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"FROM .*:latest"&lt;/span&gt; Dockerfile&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Kubernetes manifests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="s2"&gt;"image: .*:latest"&lt;/span&gt; k8s/ deploy/ helm/ &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a perfect policy engine.&lt;/p&gt;

&lt;p&gt;It will miss some cases and produce false positives.&lt;/p&gt;

&lt;p&gt;But it is a cheap starting point.&lt;/p&gt;

&lt;p&gt;Later, a team can replace this with proper policy-as-code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semgrep rules for CI files and Dockerfiles;&lt;/li&gt;
&lt;li&gt;OPA/Rego policies;&lt;/li&gt;
&lt;li&gt;Kyverno policies;&lt;/li&gt;
&lt;li&gt;GitLab or GitHub required checks;&lt;/li&gt;
&lt;li&gt;admission control in Kubernetes;&lt;/li&gt;
&lt;li&gt;registry allowlists and blocklists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rollout path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detect first
warn next
block later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to block everything on day one usually creates too much friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would block immediately
&lt;/h2&gt;

&lt;p&gt;There are a few patterns I would block immediately in production or security-sensitive CI/CD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: something:latest
docker run something:latest
FROM something:latest
uses: owner/action@master
uses: owner/action@main
curl ... | sh without version or checksum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These patterns make incident response harder.&lt;/p&gt;

&lt;p&gt;They also make reproducibility weaker.&lt;/p&gt;

&lt;p&gt;A pipeline should not depend on a moving target when it handles source code, secrets, cloud credentials, releases, or production artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would allow temporarily
&lt;/h2&gt;

&lt;p&gt;There are cases where strict digest pinning may be too much at the beginning.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local experiments;&lt;/li&gt;
&lt;li&gt;throwaway sandbox repositories;&lt;/li&gt;
&lt;li&gt;early proof-of-concepts;&lt;/li&gt;
&lt;li&gt;non-sensitive demo pipelines;&lt;/li&gt;
&lt;li&gt;temporary development branches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even there, I would avoid &lt;code&gt;latest&lt;/code&gt; when possible.&lt;/p&gt;

&lt;p&gt;But security work is about risk and rollout.&lt;/p&gt;

&lt;p&gt;The strongest policy should start where the risk is highest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;production deploy pipelines;&lt;/li&gt;
&lt;li&gt;release pipelines;&lt;/li&gt;
&lt;li&gt;cloud credential access;&lt;/li&gt;
&lt;li&gt;container publishing jobs;&lt;/li&gt;
&lt;li&gt;security scanning jobs;&lt;/li&gt;
&lt;li&gt;jobs with secrets;&lt;/li&gt;
&lt;li&gt;jobs that run on protected branches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes adoption more realistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What pinning does not solve
&lt;/h2&gt;

&lt;p&gt;Pinning is useful, but it is not magic.&lt;/p&gt;

&lt;p&gt;It does not protect you if you pin a malicious digest.&lt;/p&gt;

&lt;p&gt;It does not replace scanning.&lt;/p&gt;

&lt;p&gt;It does not replace signature verification.&lt;/p&gt;

&lt;p&gt;It does not replace least-privilege CI/CD tokens.&lt;/p&gt;

&lt;p&gt;It does not replace secret rotation.&lt;/p&gt;

&lt;p&gt;It does not replace egress controls.&lt;/p&gt;

&lt;p&gt;It does not replace runner isolation.&lt;/p&gt;

&lt;p&gt;It does not replace review of third-party tools.&lt;/p&gt;

&lt;p&gt;It does not guarantee that a previously trusted image stays safe forever.&lt;/p&gt;

&lt;p&gt;Pinning solves one specific problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;it prevents silent movement from reviewed content to different content
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is valuable, but it is only one layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger CI/CD hardening picture
&lt;/h2&gt;

&lt;p&gt;The Trivy incident is not only a lesson about tags.&lt;/p&gt;

&lt;p&gt;It is a lesson about how much trust we put into CI/CD.&lt;/p&gt;

&lt;p&gt;A stronger pipeline should combine multiple controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pin images by digest;&lt;/li&gt;
&lt;li&gt;pin GitHub Actions by full commit SHA;&lt;/li&gt;
&lt;li&gt;use least-privilege tokens;&lt;/li&gt;
&lt;li&gt;avoid long-lived credentials;&lt;/li&gt;
&lt;li&gt;use OIDC for cloud access where possible;&lt;/li&gt;
&lt;li&gt;restrict egress from runners;&lt;/li&gt;
&lt;li&gt;separate trusted and untrusted workflows;&lt;/li&gt;
&lt;li&gt;avoid running untrusted code with secrets;&lt;/li&gt;
&lt;li&gt;rotate credentials after suspicious execution;&lt;/li&gt;
&lt;li&gt;keep audit logs;&lt;/li&gt;
&lt;li&gt;use short-lived runners for sensitive jobs;&lt;/li&gt;
&lt;li&gt;scan artifacts before deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aqua's advisory also includes remediation themes such as rotating exposed secrets, auditing Trivy versions, auditing action references, and checking for exfiltration artifacts.&lt;/p&gt;

&lt;p&gt;That is the broader lesson.&lt;/p&gt;

&lt;p&gt;Pinning helps reduce unexpected dependency movement.&lt;/p&gt;

&lt;p&gt;But CI/CD hardening also needs identity, isolation, permissions, monitoring, and incident response.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this connects to my build tooling
&lt;/h2&gt;

&lt;p&gt;In my Java secure build tooling articles, the main problem was security build drift.&lt;/p&gt;

&lt;p&gt;The Gradle plugin article focuses on moving SonarQube, Dependency-Check, CycloneDX, and coverage conventions into the build system instead of duplicating scanner logic across CI/CD YAML.&lt;/p&gt;

&lt;p&gt;The Maven extension article focuses on making normal Maven lifecycle commands security-aware so the build extension owns the security wiring and CI/CD stays smaller.&lt;/p&gt;

&lt;p&gt;This article is about a different layer.&lt;/p&gt;

&lt;p&gt;Even if the build system owns the security workflow, CI/CD still runs inside images.&lt;/p&gt;

&lt;p&gt;Those images are part of the trust boundary.&lt;/p&gt;

&lt;p&gt;For example, a GitLab CI job might use:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is readable and convenient, but it is still a tag.&lt;/p&gt;

&lt;p&gt;For a stronger supply chain posture, the same principle applies:&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;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17@sha256:&amp;lt;pinned-image-digest&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build system can reduce security configuration drift.&lt;/p&gt;

&lt;p&gt;Pinned images reduce toolchain drift.&lt;/p&gt;

&lt;p&gt;Both ideas point in the same direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;security behavior should be explicit, versioned, and reviewable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fixed versions vs fixed behavior
&lt;/h2&gt;

&lt;p&gt;This is the part I think matters most.&lt;/p&gt;

&lt;p&gt;Using fixed image versions is not only about security.&lt;/p&gt;

&lt;p&gt;It is also about behavior.&lt;/p&gt;

&lt;p&gt;If a scanner changes silently, findings can change silently.&lt;/p&gt;

&lt;p&gt;If a base image changes silently, builds can change silently.&lt;/p&gt;

&lt;p&gt;If a CI action changes silently, pipeline behavior can change silently.&lt;/p&gt;

&lt;p&gt;That creates confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Was this new finding caused by new code?&lt;/li&gt;
&lt;li&gt;Was it caused by a new scanner version?&lt;/li&gt;
&lt;li&gt;Was it caused by a changed vulnerability database?&lt;/li&gt;
&lt;li&gt;Was it caused by a changed image?&lt;/li&gt;
&lt;li&gt;Was it caused by a moved tag?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security teams need signal.&lt;/p&gt;

&lt;p&gt;Floating tags add noise.&lt;/p&gt;

&lt;p&gt;Fixed versions and digests make changes easier to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I prefer
&lt;/h2&gt;

&lt;p&gt;For important CI/CD images, I prefer this workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Choose a specific version.
2. Resolve its digest.
3. Commit image:tag@sha256:digest.
4. Scan the image.
5. Run the pipeline.
6. Automate update pull requests.
7. Review updates like dependency changes.
8. Merge intentionally.
9. Keep old pipeline history understandable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not complicated.&lt;/p&gt;

&lt;p&gt;It just treats CI tooling as real dependencies.&lt;/p&gt;

&lt;p&gt;That is the mindset shift.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical team policy
&lt;/h2&gt;

&lt;p&gt;A reasonable policy could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;development:
  fixed version tags are recommended
  latest is discouraged

CI/CD with no secrets:
  fixed version tags are required
  digests are recommended

CI/CD with secrets:
  digests are required
  GitHub Actions must use full commit SHA
  no latest/main/master references

production:
  digests are required
  approved registries only
  automated update PRs required
  image scanning required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is easier to roll out than one hard rule for everything.&lt;/p&gt;

&lt;p&gt;It also matches risk.&lt;/p&gt;

&lt;p&gt;The more privileged the environment, the less acceptable floating dependencies become.&lt;/p&gt;

&lt;h2&gt;
  
  
  A simple checklist
&lt;/h2&gt;

&lt;p&gt;For every repository, I would ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do CI jobs use &lt;code&gt;:latest&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Do scanner jobs use fixed versions?&lt;/li&gt;
&lt;li&gt;Do scanner jobs use digests?&lt;/li&gt;
&lt;li&gt;Do Dockerfiles use floating base image tags?&lt;/li&gt;
&lt;li&gt;Do GitHub Actions use tags instead of full SHAs?&lt;/li&gt;
&lt;li&gt;Are image updates visible in pull requests?&lt;/li&gt;
&lt;li&gt;Do we know which image digest ran in a past pipeline?&lt;/li&gt;
&lt;li&gt;Can we block unsafe image references?&lt;/li&gt;
&lt;li&gt;Can we rotate credentials quickly if a CI tool is compromised?&lt;/li&gt;
&lt;li&gt;Are CI tokens least-privilege?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer to most of these is "no", the pipeline is probably too trusting.&lt;/p&gt;

&lt;p&gt;Not broken.&lt;/p&gt;

&lt;p&gt;But too trusting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from the Trivy incident
&lt;/h2&gt;

&lt;p&gt;The biggest lesson is not that one specific version was bad.&lt;/p&gt;

&lt;p&gt;The bigger lesson is that CI/CD pipelines often trust moving references.&lt;/p&gt;

&lt;p&gt;The Trivy incident made that visible because the compromised component was a security tool.&lt;/p&gt;

&lt;p&gt;That makes the situation feel ironic, but the engineering lesson is broader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;security tools are still dependencies
dependencies need version control
version control needs review
review needs an audit trail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mutable tag is not an audit trail.&lt;/p&gt;

&lt;p&gt;A digest is much closer to one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I do not see fixed image versions as a paranoid practice.&lt;/p&gt;

&lt;p&gt;I see them as normal supply chain hygiene.&lt;/p&gt;

&lt;p&gt;If a pipeline pulls &lt;code&gt;latest&lt;/code&gt;, the tool can change without a commit.&lt;/p&gt;

&lt;p&gt;If a pipeline uses a fixed version, the intended version is visible.&lt;/p&gt;

&lt;p&gt;If a pipeline uses a digest, the actual content is pinned.&lt;/p&gt;

&lt;p&gt;That is the progression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;latest:
  convenient but risky

fixed version:
  better and easier to adopt

version plus digest:
  stronger and more reproducible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Trivy supply chain incident showed why this matters in a very practical way.&lt;/p&gt;

&lt;p&gt;A tool used to improve security became part of the attack path.&lt;/p&gt;

&lt;p&gt;That does not mean we should stop using security tools.&lt;/p&gt;

&lt;p&gt;It means we should run them like any other high-trust dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pinned
reviewed
updated intentionally
auditable
least-privileged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security tooling should not silently change under our feet.&lt;/p&gt;

&lt;p&gt;Especially when that tooling runs inside CI/CD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/nkuziev-sec/stop-copy-pasting-security-yaml-a-gradle-build-layer-for-java-appsec-5ec7"&gt;Stop Copy-Pasting Security YAML: A Gradle Build Layer for Java AppSec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/nkuziev-sec/making-maven-builds-security-aware-appsec-checks-without-cicd-drift-2n1m"&gt;Making Maven Builds Security-Aware: AppSec Checks Without CI/CD Drift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/nkuziev-sec/dont-let-secrets-become-commits-bringing-gitleaks-into-the-developer-workflow-3hjm"&gt;Don't Let Secrets Become Commits: Bringing Gitleaks Into the Developer Workflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aquasecurity/trivy/security/advisories/GHSA-69fq-xp46-6x23" rel="noopener noreferrer"&gt;Aqua Security advisory: Trivy ecosystem supply chain temporarily compromised&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/trivy-supply-chain-compromise-what-docker-hub-users-should-know/" rel="noopener noreferrer"&gt;Docker: Trivy supply chain compromise: what Docker Hub users should know&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/build/building/best-practices/#pin-base-image-versions" rel="noopener noreferrer"&gt;Docker Docs: Building best practices, pin base image versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;GitHub Docs: Secure use reference for GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>cicd</category>
      <category>docker</category>
      <category>vulnerabilities</category>
    </item>
    <item>
      <title>Don't Let Secrets Become Commits: Bringing Gitleaks Into the Developer Workflow</title>
      <dc:creator>Nikolay Kuziev</dc:creator>
      <pubDate>Thu, 07 May 2026 04:19:46 +0000</pubDate>
      <link>https://dev.to/nkuziev-sec/dont-let-secrets-become-commits-bringing-gitleaks-into-the-developer-workflow-3hjm</link>
      <guid>https://dev.to/nkuziev-sec/dont-let-secrets-become-commits-bringing-gitleaks-into-the-developer-workflow-3hjm</guid>
      <description>&lt;p&gt;I do not like discovering secrets in CI/CD.&lt;/p&gt;

&lt;p&gt;Not because CI/CD secret scanning is useless. It is useful. It is necessary. I still want it in every serious pipeline.&lt;/p&gt;

&lt;p&gt;The problem is timing.&lt;/p&gt;

&lt;p&gt;When a token, password, private key, or API credential is detected in CI/CD, the code has already left the developer machine. It may already exist in a branch, a merge request, remote Git history, build logs, pipeline artifacts, caches, forks, or someone else's local clone. At that point the task is no longer just "remove the line from the code". The task becomes rotation, cleanup, investigation, and explaining why a preventable mistake reached shared infrastructure.&lt;/p&gt;

&lt;p&gt;That is the part I wanted to remove from the normal development loop.&lt;/p&gt;

&lt;p&gt;For secrets, the best finding is the one that appears before the commit exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

&lt;p&gt;Most teams already have security tools. They have SAST. They have dependency scanning. They have CI/CD jobs. They have repository rules. They may even have secret detection enabled at the platform level.&lt;/p&gt;

&lt;p&gt;But the first feedback often still appears after a push.&lt;/p&gt;

&lt;p&gt;That means the developer workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write code
commit
push
wait for pipeline
pipeline fails
open logs
find secret warning
fix locally
push again
maybe rotate credential
maybe clean history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a great experience for developers, and it is not a great control for security teams.&lt;/p&gt;

&lt;p&gt;A developer made the mistake locally, but the warning appears remotely. The context switch is unnecessary. The delay is unnecessary. The risk is unnecessary.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that CI/CD is often used as the first security feedback loop, when it should be the shared verification layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI-only secret scanning is too late
&lt;/h2&gt;

&lt;p&gt;A dependency vulnerability and a leaked secret are not the same class of problem.&lt;/p&gt;

&lt;p&gt;If a library has a CVE, I can usually upgrade it, add a temporary mitigation, or accept the risk while the team plans a fix.&lt;/p&gt;

&lt;p&gt;If a real credential lands in Git, I have to assume exposure. Even if the branch is private. Even if the commit is reverted. Even if the file was visible for only a few minutes. Git history and pipeline systems are not designed around pretending a committed secret never existed.&lt;/p&gt;

&lt;p&gt;This is why I treat secrets differently.&lt;/p&gt;

&lt;p&gt;For secrets, I want the first line of defense here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before commit -&amp;gt; before push -&amp;gt; before CI/CD -&amp;gt; before review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not because local checks are perfect. They are not. A developer can forget to install hooks. A hook can be bypassed. A scanner can miss something.&lt;/p&gt;

&lt;p&gt;But a local hook catches the boring, obvious, expensive mistakes before they become shared problems.&lt;/p&gt;

&lt;p&gt;That is already a win.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principle: security checks should run where the mistake happens
&lt;/h2&gt;

&lt;p&gt;The developer writes the code locally. The developer stages the change locally. The developer creates the commit locally.&lt;/p&gt;

&lt;p&gt;So a part of the security feedback should also happen locally.&lt;/p&gt;

&lt;p&gt;This matters for two reasons.&lt;/p&gt;

&lt;p&gt;First, the code does not need to leave the machine just to catch obvious mistakes. A local scanner can inspect staged changes without sending source code to a remote service.&lt;/p&gt;

&lt;p&gt;Second, the feedback is immediate. The developer does not need to wait for a pipeline, open a web UI, read a long job log, and then mentally reconnect the finding back to the line they just wrote.&lt;/p&gt;

&lt;p&gt;The better loop looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;write code
stage changes
try to commit
secret detected locally
fix it immediately
commit clean code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the kind of security control developers can actually live with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The local layer: pre-commit plus Gitleaks
&lt;/h2&gt;

&lt;p&gt;For this layer I use two pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pre-commit -&amp;gt; manages Git hooks
Gitleaks   -&amp;gt; scans for secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pre-commit&lt;/code&gt; gives the repository a shared way to define hooks. Instead of asking every developer to manually create scripts inside &lt;code&gt;.git/hooks&lt;/code&gt;, the project keeps one versioned configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.pre-commit-config.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gitleaks does the secret scanning. In this setup I care about one specific behavior: run the scan before Git creates the commit.&lt;/p&gt;

&lt;p&gt;A minimal configuration looks like this:&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;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/gitleaks/gitleaks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v8.24.2&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the developer installs the hook once:&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="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before trusting it on an existing repository, I usually run the hook across the current tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pre-commit run &lt;span class="nt"&gt;--all-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the normal flow stays normal:&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 &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Implement payment validation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is what happens between &lt;code&gt;git commit&lt;/code&gt; and the commit actually being created. The hook runs. Gitleaks scans the staged change. If it sees a likely secret, the commit stops locally.&lt;/p&gt;

&lt;p&gt;No branch. No merge request. No pipeline artifact. No shared Git history.&lt;/p&gt;

&lt;p&gt;Just a local warning at the point where the developer can fix it fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  I do not want this to become a README
&lt;/h2&gt;

&lt;p&gt;The value is not the YAML snippet.&lt;/p&gt;

&lt;p&gt;The value is the placement of the control.&lt;/p&gt;

&lt;p&gt;A pre-commit hook is not a security program. It is not a replacement for CI/CD scanning, repository protection, secret management, review, or credential rotation. It is one small local safety net.&lt;/p&gt;

&lt;p&gt;But it changes the tone of the workflow.&lt;/p&gt;

&lt;p&gt;Instead of security saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You pushed a secret. Now go clean it up.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This looks like a secret.
Do not commit it.
Move it somewhere safe.
Try again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a completely different developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project-specific Gitleaks configuration
&lt;/h2&gt;

&lt;p&gt;Default rules are useful, but real projects usually need a little tuning.&lt;/p&gt;

&lt;p&gt;Test fixtures, documentation examples, generated samples, and fake tokens can produce false positives. I do not want developers fighting the tool every day, but I also do not want a giant allowlist that hides real problems.&lt;/p&gt;

&lt;p&gt;A small &lt;code&gt;.gitleaks.toml&lt;/code&gt; is usually enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Project Gitleaks Configuration"&lt;/span&gt;

&lt;span class="nn"&gt;[extend]&lt;/span&gt;
&lt;span class="py"&gt;useDefault&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[[allowlists]]&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Allow fake examples in documentation and test fixtures"&lt;/span&gt;
&lt;span class="py"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'''docs/examples/.*'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'''src/test/resources/.*'''&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[allowlists]]&lt;/span&gt;
&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Allow clearly fake placeholder values"&lt;/span&gt;
&lt;span class="py"&gt;regexTarget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"line"&lt;/span&gt;
&lt;span class="py"&gt;regexes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'''not-a-real-secret'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'''example-token'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'''dummy-password'''&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My rule for allowlists is simple: they should explain why the value is safe.&lt;/p&gt;

&lt;p&gt;If the answer is "because it is annoying", that is not a good allowlist. If the answer is "because this directory contains fake examples used in tests", that is much better.&lt;/p&gt;

&lt;p&gt;When Gitleaks reports something, the first question should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is this a real credential?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How do I silence the scanner?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is real, the fix is to remove it from code, move it to environment variables or a secret manager, rotate it if needed, and check whether it reached remote history.&lt;/p&gt;

&lt;p&gt;The allowlist is for false positives. It is not a trash bin for uncomfortable findings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where pre-commit ends and build tooling begins
&lt;/h2&gt;

&lt;p&gt;I do not put every security check into &lt;code&gt;pre-commit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is how good controls become hated controls.&lt;/p&gt;

&lt;p&gt;A commit hook should be fast enough that developers do not look for ways around it. Secret scanning is a good fit because the impact is high and the check can be quick. Formatting, simple linting, YAML/JSON validation, and accidental large-file detection also fit well.&lt;/p&gt;

&lt;p&gt;Heavier checks belong somewhere else.&lt;/p&gt;

&lt;p&gt;The way I model it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pre-commit
  fast checks: secrets, formatting, simple file validation

pre-push
  medium checks: broader scans, policy validation, selected tests

build tooling layer
  full local project checks: SCA, SBOM, coverage, SonarQube metadata

CI/CD
  clean shared execution, gates, artifacts, enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where my Java secure build tooling fits in.&lt;/p&gt;

&lt;p&gt;For Gradle projects, I want a command like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew clean securityAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Maven projects, I want the normal lifecycle to carry the security workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That build layer can run Dependency-Check, generate a CycloneDX SBOM, prepare coverage, and configure SonarQube metadata. It is heavier than a pre-commit hook, but it is still local-friendly and CI/CD-ready.&lt;/p&gt;

&lt;p&gt;The key is that each layer has a job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gitleaks + pre-commit
  catches secrets before commit

Gradle/Maven build tooling
  runs repeatable AppSec checks locally and in CI/CD

CI/CD
  verifies the same workflow in a clean shared environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps security close to the developer without turning every commit into a full compliance audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD still stays in the design
&lt;/h2&gt;

&lt;p&gt;Local hooks are not enough.&lt;/p&gt;

&lt;p&gt;Someone can skip hook installation. A machine can be misconfigured. Automation can push code. A developer can bypass a hook intentionally. That is why I still run Gitleaks in CI/CD.&lt;/p&gt;

&lt;p&gt;The design principle is not "local instead of CI".&lt;/p&gt;

&lt;p&gt;The principle is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local for fast feedback
CI/CD for enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple CI job can use the same idea:&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;secret_scan:gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&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;zricethezav/gitleaks:v8.24.2&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&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;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;gitleaks git --redact --verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For direct local scans outside pre-commit, I prefer the current Gitleaks command style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitleaks git &lt;span class="nt"&gt;--redact&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;
gitleaks &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;--redact&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives security teams a shared safety net while still giving developers a faster local warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers get
&lt;/h2&gt;

&lt;p&gt;Developers do not need another portal to check before every commit.&lt;/p&gt;

&lt;p&gt;They need a clear signal in the workflow they already use.&lt;/p&gt;

&lt;p&gt;A good local secret scanning setup gives them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feedback before the mistake becomes Git history;&lt;/li&gt;
&lt;li&gt;no need to upload code to catch obvious secrets;&lt;/li&gt;
&lt;li&gt;fewer surprise pipeline failures;&lt;/li&gt;
&lt;li&gt;a small, repeatable command set;&lt;/li&gt;
&lt;li&gt;less back-and-forth with security for simple mistakes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part is that the fix usually happens while the developer still remembers what they changed.&lt;/p&gt;

&lt;p&gt;That matters more than people admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the security team gets
&lt;/h2&gt;

&lt;p&gt;Security teams get fewer noisy incidents and a better place to enforce basic hygiene.&lt;/p&gt;

&lt;p&gt;Instead of treating every obvious secret as a pipeline incident, the team can push the simple feedback left and reserve CI/CD for shared verification.&lt;/p&gt;

&lt;p&gt;They also get a cleaner story for Secure SDLC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before commit: local secret scanning
before push: optional broader hooks
build: SCA, SBOM, coverage, SonarQube metadata
CI/CD: artifacts, gates, audit trail
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is much easier to explain, maintain, and improve than a pile of disconnected scanner jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters
&lt;/h2&gt;

&lt;p&gt;This is not really about Gitleaks.&lt;/p&gt;

&lt;p&gt;Gitleaks is the tool I use under the hood. The more important idea is that security checks should be placed where they reduce cost the most.&lt;/p&gt;

&lt;p&gt;For secrets, that place is before the commit.&lt;/p&gt;

&lt;p&gt;For dependency analysis and SBOM generation, that place is usually the build tooling layer, with the same behavior available locally and in CI/CD.&lt;/p&gt;

&lt;p&gt;For enforcement, that place is CI/CD.&lt;/p&gt;

&lt;p&gt;When those layers work together, security stops feeling like an external inspection step and starts feeling like normal engineering feedback.&lt;/p&gt;

&lt;p&gt;That is the workflow I want: fast locally, reproducible in CI/CD, and boring in the best possible way.&lt;/p&gt;

</description>
      <category>security</category>
      <category>git</category>
      <category>devops</category>
      <category>developers</category>
    </item>
    <item>
      <title>Making Maven Builds Security-Aware: AppSec Checks Without CI/CD Drift</title>
      <dc:creator>Nikolay Kuziev</dc:creator>
      <pubDate>Thu, 07 May 2026 04:13:38 +0000</pubDate>
      <link>https://dev.to/nkuziev-sec/making-maven-builds-security-aware-appsec-checks-without-cicd-drift-2n1m</link>
      <guid>https://dev.to/nkuziev-sec/making-maven-builds-security-aware-appsec-checks-without-cicd-drift-2n1m</guid>
      <description>&lt;p&gt;The problem was never that Maven projects could not run security tools.&lt;/p&gt;

&lt;p&gt;They could.&lt;/p&gt;

&lt;p&gt;A pipeline can run tests, Dependency-Check, CycloneDX, and SonarQube with a few commands. A &lt;code&gt;pom.xml&lt;/code&gt; can hold plugin blocks. A team can copy a working configuration from one service to another and call it a standard.&lt;/p&gt;

&lt;p&gt;For a while, that works.&lt;/p&gt;

&lt;p&gt;Then the small differences start showing up.&lt;/p&gt;

&lt;p&gt;One service has JaCoCo but does not pass the XML report to SonarQube. Another produces Dependency-Check output only as HTML. One multi-module project generates an SBOM from the root aggregator and misses the shape of the real runtime application. Another pipeline forgets merge request metadata, so SonarQube analysis is technically successful but practically incomplete.&lt;/p&gt;

&lt;p&gt;That is security build drift.&lt;/p&gt;

&lt;p&gt;It looks like automation. It behaves like inconsistency.&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;secure-maven-extension&lt;/code&gt; to solve that problem for Maven projects.&lt;/p&gt;

&lt;p&gt;Not by replacing the scanners.&lt;/p&gt;

&lt;p&gt;By making the Maven lifecycle carry the security workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to remove
&lt;/h2&gt;

&lt;p&gt;A typical Maven CI/CD setup starts like this:&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;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw org.owasp:dependency-check-maven:check&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw org.cyclonedx:cyclonedx-maven-plugin:makeBom&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw sonar:sonar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For one repository, this is fine.&lt;/p&gt;

&lt;p&gt;Across many services, it becomes a maintenance pattern nobody really owns.&lt;/p&gt;

&lt;p&gt;Some configuration lives in CI/CD. Some lives in &lt;code&gt;pom.xml&lt;/code&gt;. Some lives in copied documentation. Some depends on environment variables that are not obvious to local developers. Every new service has to rediscover the same setup decisions.&lt;/p&gt;

&lt;p&gt;The result is not only duplicated YAML.&lt;/p&gt;

&lt;p&gt;The result is lower confidence.&lt;/p&gt;

&lt;p&gt;If local runs do not match CI/CD, developers push just to test the security workflow. If reports are produced in different places, security teams waste time normalizing artifacts. If multi-module projects are wired differently, nobody knows whether the SBOM actually describes the deployable artifact.&lt;/p&gt;

&lt;p&gt;At that point, the build is not security-aware. The pipeline is just calling scanners around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the usual approach is inconvenient
&lt;/h2&gt;

&lt;p&gt;The usual approach puts too much responsibility into pipeline scripts.&lt;/p&gt;

&lt;p&gt;CI/CD should be the shared execution layer. It should run clean builds, publish artifacts, enforce gates, and provide auditability.&lt;/p&gt;

&lt;p&gt;But when CI/CD also owns all scanner configuration, every repository becomes a custom integration point.&lt;/p&gt;

&lt;p&gt;That makes local development awkward.&lt;/p&gt;

&lt;p&gt;A developer can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the pipeline may run a different set of goals, with different properties, report formats, and SonarQube metadata. So the developer cannot fully trust the local result.&lt;/p&gt;

&lt;p&gt;This is the gap I wanted to close.&lt;/p&gt;

&lt;p&gt;The Maven command should stay familiar, but the lifecycle should carry the same AppSec behavior locally and in CI/CD.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;The core rule was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keep the Maven user experience native,
but inject repeatable security behavior into the lifecycle.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers should not need a separate security script for every service. CI/CD should not need to reimplement scanner conventions. Security teams should not need to explain report paths and plugin settings repository by repository.&lt;/p&gt;

&lt;p&gt;The build should know how to do the boring parts.&lt;/p&gt;

&lt;p&gt;That is why this project is a Maven core extension rather than just another command in a pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Maven core extension
&lt;/h2&gt;

&lt;p&gt;A normal Maven plugin would still require explicit plugin configuration across projects. That can work, but it does not fully remove copy-paste.&lt;/p&gt;

&lt;p&gt;A core extension gives an earlier and more powerful integration point.&lt;/p&gt;

&lt;p&gt;The extension is loaded from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.mvn/extensions.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;extensions&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;extension&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.niki1337.securebuild&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;secure-maven-extension&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/extension&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/extensions&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally, the extension works during Maven's &lt;code&gt;afterProjectsRead&lt;/code&gt; stage.&lt;/p&gt;

&lt;p&gt;That timing matters.&lt;/p&gt;

&lt;p&gt;At that point, Maven has read the root &lt;code&gt;pom.xml&lt;/code&gt; and module POMs. Packaging is known. Modules are visible. Existing plugins and properties can be inspected. But the lifecycle has not started yet.&lt;/p&gt;

&lt;p&gt;That is the useful moment to inject conventions.&lt;/p&gt;

&lt;p&gt;The extension can decide how to configure coverage, Dependency-Check, CycloneDX, and SonarQube before phases like &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;package&lt;/code&gt;, and &lt;code&gt;verify&lt;/code&gt; execute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs under the hood
&lt;/h2&gt;

&lt;p&gt;The extension connects the tools teams already know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;jacoco-maven-plugin&lt;/code&gt; for coverage;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sonar-maven-plugin&lt;/code&gt; for SonarQube analysis;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dependency-check-maven&lt;/code&gt; for dependency risk reports;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cyclonedx-maven-plugin&lt;/code&gt; for SBOM generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer still uses Maven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn package
mvn verify
mvn sonar:sonar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is that these commands become security-aware.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can build the application and generate a CycloneDX SBOM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can run tests, generate JaCoCo coverage, and execute Dependency-Check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn verify sonar:sonar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can send SonarQube analysis with branch, merge request, binary, and coverage metadata already prepared.&lt;/p&gt;

&lt;p&gt;That is the whole point: the workflow feels like Maven, not like a pile of scanner commands glued around Maven.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration without forcing one style
&lt;/h2&gt;

&lt;p&gt;Real environments are messy.&lt;/p&gt;

&lt;p&gt;Local developers may use &lt;code&gt;-D...&lt;/code&gt; properties. CI/CD usually provides environment variables. Some stable project defaults belong in &lt;code&gt;pom.xml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The extension supports all of those sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;environment variables;&lt;/li&gt;
&lt;li&gt;Maven user properties;&lt;/li&gt;
&lt;li&gt;project properties from &lt;code&gt;pom.xml&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;system properties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A project can define stable defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;secure.serviceName&amp;gt;&lt;/span&gt;payment-api&lt;span class="nt"&gt;&amp;lt;/secure.serviceName&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;sonar.projectKey&amp;gt;&lt;/span&gt;payment-api&lt;span class="nt"&gt;&amp;lt;/sonar.projectKey&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;sonar.projectName&amp;gt;&lt;/span&gt;Payment API&lt;span class="nt"&gt;&amp;lt;/sonar.projectName&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CI/CD can provide secrets and environment-specific values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"payment-api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_HOST_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://sonarqube.example.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_PROJECT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"payment-api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"token-value"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DT_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://dependency-track.example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local developer can override when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-Dsecure&lt;/span&gt;.serviceName&lt;span class="o"&gt;=&lt;/span&gt;payment-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-Dsonar&lt;/span&gt;.projectKey&lt;span class="o"&gt;=&lt;/span&gt;payment-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to force one configuration style. The goal is to make the resolved behavior consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage should not require repeated wiring
&lt;/h2&gt;

&lt;p&gt;Coverage is one of those details that quietly breaks AppSec workflows.&lt;/p&gt;

&lt;p&gt;SonarQube can run without coverage, but the result is weaker. JaCoCo can generate a report, but if XML output is missing or the path is not passed to SonarQube, the analysis is incomplete.&lt;/p&gt;

&lt;p&gt;The extension injects JaCoCo for Java &lt;code&gt;jar&lt;/code&gt; and &lt;code&gt;war&lt;/code&gt; projects when JaCoCo is not already configured.&lt;/p&gt;

&lt;p&gt;It wires the lifecycle like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initialize -&amp;gt; jacoco:prepare-agent
verify     -&amp;gt; jacoco:report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The XML report is generated at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target/site/jacoco/jacoco.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the extension passes that path into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonar.coverage.jacoco.xmlReportPaths
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not exciting work. That is exactly why it should be automated.&lt;/p&gt;

&lt;p&gt;Repeated boilerplate is where drift hides.&lt;/p&gt;

&lt;h2&gt;
  
  
  SonarQube needs more than a token
&lt;/h2&gt;

&lt;p&gt;A common mistake is treating SonarQube setup as three variables: URL, project key, token.&lt;/p&gt;

&lt;p&gt;For Java services, useful analysis also depends on source paths, test paths, compiled binaries, coverage XML, branch metadata, and merge request metadata.&lt;/p&gt;

&lt;p&gt;The extension prepares properties such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonar.sources
sonar.tests
sonar.java.binaries
sonar.java.test.binaries
sonar.coverage.jacoco.xmlReportPaths
sonar.exclusions
sonar.test.exclusions
sonar.cpd.exclusions
sonar.coverage.exclusions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitLab merge request pipelines, it can map CI variables into pull request analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI_MERGE_REQUEST_IID                  -&amp;gt; sonar.pullrequest.key
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME   -&amp;gt; sonar.pullrequest.branch
CI_MERGE_REQUEST_TARGET_BRANCH_NAME   -&amp;gt; sonar.pullrequest.base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For normal branch pipelines, it sets branch analysis metadata.&lt;/p&gt;

&lt;p&gt;This is the kind of logic that becomes fragile when copied into every pipeline file. Inside a core extension, the behavior is versioned and reusable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency-Check should produce one predictable shape
&lt;/h2&gt;

&lt;p&gt;Dependency-Check is most useful when the output is predictable.&lt;/p&gt;

&lt;p&gt;The extension injects Dependency-Check into the lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;single-module: verify -&amp;gt; dependency-check:check
multi-module:  verify -&amp;gt; dependency-check:aggregate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It standardizes report formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML
JSON
SARIF
XML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and writes reports to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target/reports/dependency-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, it disables network-dependent analyzers such as RetireJS, Node audit, Node package analyzer, OSS Index, and hosted suppressions. In restricted CI/CD environments, depending on external services can make builds slow, flaky, or inconsistent.&lt;/p&gt;

&lt;p&gt;When an internal mirror exists, the extension can use it through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DT_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://dependency-track.example.com mvn verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default adoption path is visibility first. The build can generate reports without immediately failing by CVSS score. After the team understands the findings and noise level, policy gates can become stricter.&lt;/p&gt;

&lt;p&gt;That is how I prefer to roll out AppSec checks: start with reliable data, then enforce deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  SBOM generation should describe the real artifact
&lt;/h2&gt;

&lt;p&gt;An SBOM is not useful just because it exists.&lt;/p&gt;

&lt;p&gt;It should describe the thing the team actually ships.&lt;/p&gt;

&lt;p&gt;For a single-module Maven application, the extension can run CycloneDX during &lt;code&gt;package&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package -&amp;gt; cyclonedx:makeBom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reports are written to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;target/reports/cyclonedx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SBOM focuses on compile and runtime dependencies and avoids test, provided, and system scopes.&lt;/p&gt;

&lt;p&gt;Multi-module builds need more care.&lt;/p&gt;

&lt;p&gt;The root project is often only an aggregator. Generating an SBOM there can be less meaningful than generating it from the deployable application module. For Spring Boot projects, the extension looks for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;org.springframework.boot:spring-boot-maven-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it finds a deployable Spring Boot module, it injects CycloneDX there. If not, it falls back to aggregate SBOM generation on the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package -&amp;gt; cyclonedx:makeAggregateBom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps SBOM generation tied to the application shape instead of blindly producing a file wherever Maven happens to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-module Maven projects need first-class handling
&lt;/h2&gt;

&lt;p&gt;Multi-module Maven builds are where simple CI snippets start to fall apart.&lt;/p&gt;

&lt;p&gt;A root project may have &lt;code&gt;pom&lt;/code&gt; packaging. Modules may be &lt;code&gt;jar&lt;/code&gt; or &lt;code&gt;war&lt;/code&gt;. Some modules are deployable, some are libraries, some are test fixtures. Coverage should be generated per Java module. Dependency-Check may need aggregate behavior. SonarQube needs paths that reflect the whole project.&lt;/p&gt;

&lt;p&gt;The extension treats a build as multi-module when Maven sees more than one project and simple mode is not forced. It includes Java modules with &lt;code&gt;jar&lt;/code&gt; and &lt;code&gt;war&lt;/code&gt; packaging and supports filters like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;secure.includedModules&amp;gt;&lt;/span&gt;api,service&lt;span class="nt"&gt;&amp;lt;/secure.includedModules&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;secure.excludedModules&amp;gt;&lt;/span&gt;test-fixtures&lt;span class="nt"&gt;&amp;lt;/secure.excludedModules&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In multi-module mode, it configures SonarQube on the root, injects JaCoCo into Java modules, adds module-level paths, runs aggregate Dependency-Check, and generates SBOM output from the most useful module when possible.&lt;/p&gt;

&lt;p&gt;That is the difference between running a scanner and owning a build convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD becomes an execution layer
&lt;/h2&gt;

&lt;p&gt;Once Maven owns the conventions, CI/CD can stay small.&lt;/p&gt;

&lt;p&gt;A security job can be simple:&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;security:maven&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw -B verify&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&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;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7 days&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;target/reports/dependency-check/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;target/reports/cyclonedx/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/target/reports/dependency-check/"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/target/reports/cyclonedx/"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/target/site/jacoco/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SonarQube can run only when a token is available:&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;sonarqube:maven&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./mvnw -B verify sonar:sonar&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$SONAR_TOKEN'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline is now readable because the security wiring is no longer scattered through the YAML.&lt;/p&gt;

&lt;p&gt;The build owns the behavior. CI/CD runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where pre-commit and Gitleaks fit
&lt;/h2&gt;

&lt;p&gt;The Maven extension is not the earliest security layer.&lt;/p&gt;

&lt;p&gt;For secrets, I want feedback before the commit exists. That is where &lt;code&gt;pre-commit&lt;/code&gt; and Gitleaks fit. A local secret scanning hook can stop obvious leaks before code leaves the developer machine.&lt;/p&gt;

&lt;p&gt;The Maven extension handles the next layer: build-time checks that understand Java, dependencies, coverage, SBOM generation, and SonarQube metadata.&lt;/p&gt;

&lt;p&gt;The model is layered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before commit
  pre-commit hooks, Gitleaks, fast file checks

local build
  mvn verify, coverage, Dependency-Check, CycloneDX SBOM

CI/CD
  same Maven lifecycle, artifacts, gates, enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important because not every check belongs in the same place.&lt;/p&gt;

&lt;p&gt;Secret scanning is fast and high-impact, so it belongs very early. Dependency analysis and SBOM generation are heavier and build-aware, so they belong in the build. Final enforcement belongs in CI/CD.&lt;/p&gt;

&lt;p&gt;That separation keeps the workflow practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers get
&lt;/h2&gt;

&lt;p&gt;Developers get to keep using Maven.&lt;/p&gt;

&lt;p&gt;They do not need to memorize a custom AppSec script for every repository. They do not need to push a branch just to learn whether Dependency-Check or SonarQube wiring works. They can run familiar lifecycle commands and get security-aware behavior locally.&lt;/p&gt;

&lt;p&gt;That makes findings easier to understand. The report appears in the same build context where the code was changed.&lt;/p&gt;

&lt;p&gt;This matters because good security tooling is not only about detection. It is also about timing, clarity, and trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the security team gets
&lt;/h2&gt;

&lt;p&gt;The security team gets fewer custom integrations to chase.&lt;/p&gt;

&lt;p&gt;Reports are generated in predictable formats and locations. SBOM scope becomes more consistent. Coverage is wired into SonarQube. Merge request metadata is handled in one reusable layer. Multi-module projects stop being a special case every time.&lt;/p&gt;

&lt;p&gt;This also makes policy easier to evolve.&lt;/p&gt;

&lt;p&gt;The team can start with visibility, collect reports, understand noise, and then introduce stricter gates when the data is reliable.&lt;/p&gt;

&lt;p&gt;That is much better than turning on hard failures before anyone trusts the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;secure-maven-extension&lt;/code&gt; is not another security scanner.&lt;/p&gt;

&lt;p&gt;It is a build tooling layer for Maven-based Java projects.&lt;/p&gt;

&lt;p&gt;It moves repeated AppSec wiring out of CI/CD YAML and into the Maven lifecycle, where developers can run it locally and CI/CD can reproduce it cleanly.&lt;/p&gt;

&lt;p&gt;The larger pattern is the same one I use across the whole workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local hooks for fast mistakes
build tooling for repeatable project checks
CI/CD for shared verification and enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that pattern works, security stops being an external script attached to the project and becomes part of how the project is built.&lt;/p&gt;

&lt;p&gt;That is the real goal.&lt;/p&gt;

&lt;p&gt;Project links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Niki-1337/secure-build-maven-extension" rel="noopener noreferrer"&gt;Secure Build Maven Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Niki-1337/secure-build-gradle-plugin" rel="noopener noreferrer"&gt;Secure Build Gradle Plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>cicd</category>
      <category>java</category>
      <category>mvn</category>
    </item>
    <item>
      <title>Stop Copy-Pasting Security YAML: A Gradle Build Layer for Java AppSec</title>
      <dc:creator>Nikolay Kuziev</dc:creator>
      <pubDate>Thu, 07 May 2026 04:12:22 +0000</pubDate>
      <link>https://dev.to/nkuziev-sec/stop-copy-pasting-security-yaml-a-gradle-build-layer-for-java-appsec-5ec7</link>
      <guid>https://dev.to/nkuziev-sec/stop-copy-pasting-security-yaml-a-gradle-build-layer-for-java-appsec-5ec7</guid>
      <description>&lt;p&gt;The hard part of Java AppSec is usually not finding another scanner.&lt;/p&gt;

&lt;p&gt;Most teams already have the scanners.&lt;/p&gt;

&lt;p&gt;They have SonarQube for code analysis. They have OWASP Dependency-Check for dependency risk. They have CycloneDX for SBOM generation. They have JaCoCo or Kover for coverage. They have GitLab CI, GitHub Actions, Jenkins, or something similar to run all of it.&lt;/p&gt;

&lt;p&gt;And still, the workflow drifts.&lt;/p&gt;

&lt;p&gt;One repository writes Dependency-Check reports to one path. Another produces only HTML. One pipeline sends merge request metadata to SonarQube correctly. Another accidentally runs branch analysis for everything. One service generates an SBOM from runtime dependencies. Another includes test dependencies and makes the report noisy. A multi-module project needs a special exception, so someone copies another YAML block and edits it until the pipeline is green.&lt;/p&gt;

&lt;p&gt;None of this looks dramatic on day one.&lt;/p&gt;

&lt;p&gt;After a few months, it becomes security build drift.&lt;/p&gt;

&lt;p&gt;That is the problem I wanted to solve with &lt;code&gt;secure-build-gradle-plugin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not by inventing a new scanner.&lt;/p&gt;

&lt;p&gt;By moving security wiring into a reusable Gradle build layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I ran into
&lt;/h2&gt;

&lt;p&gt;The usual DevSecOps starting point is CI/CD YAML.&lt;/p&gt;

&lt;p&gt;For a single Java service, this is fine:&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;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew dependencyCheckAnalyze&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew cyclonedxBom&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew sonar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is direct. It is visible. It works.&lt;/p&gt;

&lt;p&gt;The problem starts when the same idea spreads across many repositories.&lt;/p&gt;

&lt;p&gt;Every service becomes responsible for remembering how security tools should be configured. Every team copies a slightly different version. Every pipeline slowly becomes a custom integration.&lt;/p&gt;

&lt;p&gt;The failure mode is not only technical. It affects trust.&lt;/p&gt;

&lt;p&gt;Developers do not know which command reproduces the pipeline locally. Security teams do not know whether two reports were generated with the same assumptions. CI/CD becomes full of scanner plumbing instead of clear build steps.&lt;/p&gt;

&lt;p&gt;At that point, the issue is not "we need more tools".&lt;/p&gt;

&lt;p&gt;The issue is "we need one place for the conventions".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI/CD-only security wiring is uncomfortable
&lt;/h2&gt;

&lt;p&gt;CI/CD is good at clean execution. It gives a fresh environment, shared logs, artifacts, gates, and a common place for enforcement.&lt;/p&gt;

&lt;p&gt;But CI/CD is not a great place to own all security behavior.&lt;/p&gt;

&lt;p&gt;When the logic lives only in pipeline files, local development becomes second-class. Developers push to find out what the security workflow thinks. If a report path is wrong, the pipeline fails. If SonarQube metadata is incomplete, the analysis is misleading. If Dependency-Check output differs between services, the security team has to normalize the mess later.&lt;/p&gt;

&lt;p&gt;This creates the worst kind of friction: nobody is arguing about risk yet. Everyone is arguing about tool wiring.&lt;/p&gt;

&lt;p&gt;I wanted CI/CD to execute the security workflow, not define it from scratch in every repository.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design principle
&lt;/h2&gt;

&lt;p&gt;The rule I used for the Gradle plugin was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;security behavior should live close to the code,
and CI/CD should run the same behavior without reimplementing it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means developers should be able to run the same checks before opening a merge request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew clean securityAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And CI/CD should be able to run the same task and collect predictable reports.&lt;/p&gt;

&lt;p&gt;The result should not depend on whether the command was executed on a laptop or inside a pipeline, except for environment-specific details like tokens and branch metadata.&lt;/p&gt;

&lt;p&gt;That is the point of the build tooling layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build tooling layer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;secure-build-gradle-plugin&lt;/code&gt; is a Gradle convention plugin.&lt;/p&gt;

&lt;p&gt;A project applies the plugin once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s2"&gt;"java"&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s2"&gt;"io.github.niki1337.securebuild.gradle-java"&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the project keeps only project-specific values in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;securityConventions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payment-api"&lt;/span&gt;
  &lt;span class="n"&gt;sonarProjectKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payment-api"&lt;/span&gt;
  &lt;span class="n"&gt;allowLocalSonar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a multi-module project, the root build can describe which modules matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;plugins&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="s2"&gt;"io.github.niki1337.securebuild.gradle-java"&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;securityConventions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;serviceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-platform"&lt;/span&gt;
  &lt;span class="n"&gt;sonarProjectKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments-platform"&lt;/span&gt;
  &lt;span class="n"&gt;includedModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"service"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;excludedModules&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test-fixtures"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changes the ownership model.&lt;/p&gt;

&lt;p&gt;CI/CD still runs the checks. Security still owns policy. Developers still fix findings.&lt;/p&gt;

&lt;p&gt;But the repeated scanner wiring lives in the build system, versioned as engineering code instead of copy-pasted as pipeline folklore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs under the hood
&lt;/h2&gt;

&lt;p&gt;The plugin connects existing tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SonarQube analysis;&lt;/li&gt;
&lt;li&gt;OWASP Dependency-Check;&lt;/li&gt;
&lt;li&gt;CycloneDX SBOM generation;&lt;/li&gt;
&lt;li&gt;JaCoCo or Kover coverage;&lt;/li&gt;
&lt;li&gt;Gradle single-module and multi-module behavior;&lt;/li&gt;
&lt;li&gt;Git branch and GitLab merge request metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin is not trying to replace those tools. That would be the wrong abstraction.&lt;/p&gt;

&lt;p&gt;The value is in making them behave consistently.&lt;/p&gt;

&lt;p&gt;A developer gets one practical entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew clean securityAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That task can run tests, produce coverage, generate SBOM output, and run dependency analysis. When a developer needs to inspect individual pieces, the underlying tasks are still there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew cyclonedxDirectBom &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
./gradlew dependencyCheckAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
./gradlew sonarHelp &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For multi-module builds, aggregate analysis stays explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew dependencyCheckAggregate &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not to hide the tools. The point is to make the normal path obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving feedback before the merge request
&lt;/h2&gt;

&lt;p&gt;One of the biggest wins is psychological.&lt;/p&gt;

&lt;p&gt;If AppSec checks run only after a push, developers treat findings as pipeline problems. If they can run the same workflow locally, findings become engineering feedback.&lt;/p&gt;

&lt;p&gt;That is a healthier model.&lt;/p&gt;

&lt;p&gt;A developer can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew securityAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before opening a merge request. They can see dependency findings, SBOM output, coverage reports, and local scanner artifacts before the branch becomes a shared review object.&lt;/p&gt;

&lt;p&gt;CI/CD still matters. I do not trust laptops as the final gate. But local execution reduces surprise, and reducing surprise is one of the best ways to make security tooling accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  SonarQube metadata belongs in conventions
&lt;/h2&gt;

&lt;p&gt;SonarQube is easy to configure almost correctly.&lt;/p&gt;

&lt;p&gt;That is the dangerous part.&lt;/p&gt;

&lt;p&gt;The scanner may run, the job may pass, and the analysis may still be incomplete because Java binaries, libraries, coverage XML, branch names, or merge request metadata were not passed correctly.&lt;/p&gt;

&lt;p&gt;That is why I do not want every repository hand-writing this logic.&lt;/p&gt;

&lt;p&gt;The plugin resolves SonarQube configuration from environment variables, Gradle properties, or the &lt;code&gt;securityConventions&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_HOST_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://sonarqube.example.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_PROJECT_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"payment-api"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SONAR_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"token-value"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it prepares the Java analysis properties that teams usually forget at least once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonar.sources
sonar.tests
sonar.java.binaries
sonar.java.test.binaries
sonar.java.libraries
sonar.java.test.libraries
sonar.coverage.jacoco.xmlReportPaths
sonar.exclusions
sonar.test.exclusions
sonar.cpd.exclusions
sonar.coverage.exclusions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitLab merge request pipelines, it can map CI variables to SonarQube pull request properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI_MERGE_REQUEST_IID                  -&amp;gt; sonar.pullrequest.key
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME   -&amp;gt; sonar.pullrequest.branch
CI_MERGE_REQUEST_TARGET_BRANCH_NAME   -&amp;gt; sonar.pullrequest.base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For branch pipelines, it sets branch analysis metadata instead.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of boring detail that should be solved once and reused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency-Check reports should be boring
&lt;/h2&gt;

&lt;p&gt;Dependency-Check is useful when reports are predictable.&lt;/p&gt;

&lt;p&gt;I do not want one service producing JSON, another producing only HTML, and another hiding reports in a custom directory. That creates unnecessary work downstream.&lt;/p&gt;

&lt;p&gt;The plugin standardizes formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML
JSON
SARIF
XML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and writes reports to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/reports/dependency-check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, it avoids depending on network-heavy analyzers that can make CI/CD slow or unstable in restricted environments, such as OSS Index, RetireJS, Node audit, Node package analysis, hosted suppressions, and CISA KEV analyzer.&lt;/p&gt;

&lt;p&gt;If an internal mirror exists, the build can use it through configuration like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DT_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://dependency-track.example.com &lt;span class="se"&gt;\&lt;/span&gt;
./gradlew dependencyCheckAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default behavior is visibility first, not instant blocking. A build can generate useful reports without failing by CVSS score immediately. Once the team understands the noise and triage process, stricter gates can be introduced deliberately.&lt;/p&gt;

&lt;p&gt;That is a practical AppSec adoption path.&lt;/p&gt;

&lt;h2&gt;
  
  
  SBOM output should describe the runtime artifact
&lt;/h2&gt;

&lt;p&gt;SBOM generation is not valuable just because a file exists.&lt;/p&gt;

&lt;p&gt;The SBOM has to describe something useful.&lt;/p&gt;

&lt;p&gt;If one service includes test dependencies and another does not, comparison becomes messy. If a multi-module root project is only an aggregator, a root-level SBOM may say very little about the deployable application.&lt;/p&gt;

&lt;p&gt;The plugin configures CycloneDX with runtime-oriented output in mind:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runtime dependencies included
test dependencies skipped
license text not embedded
BOM serial number disabled
metadata noise reduced
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typical output paths stay predictable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/reports/cyclonedx
build/reports/cyclonedx-direct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Spring Boot style multi-module projects, the useful SBOM often comes from the deployable module, not the root. The plugin tries to make that the default path while still copying reports back to predictable root locations for CI/CD artifact collection.&lt;/p&gt;

&lt;p&gt;This is the kind of detail that looks small until you have ten services and every one produces a different SBOM shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage wiring should not be tribal knowledge
&lt;/h2&gt;

&lt;p&gt;Coverage is another source of drift.&lt;/p&gt;

&lt;p&gt;Some projects use JaCoCo. Some already use Kover. Some generate XML. Some do not. Some pass the report to SonarQube. Some forget.&lt;/p&gt;

&lt;p&gt;The plugin supports a simple default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;securityConventions&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;coverageProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"auto"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;auto&lt;/code&gt; mode, it uses Kover when Kover is already present. Otherwise, it applies and configures JaCoCo.&lt;/p&gt;

&lt;p&gt;For JaCoCo, it enables XML output and wires the standard path into SonarQube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/reports/jacoco/test/jacocoTestReport.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, not glamorous. Just useful.&lt;/p&gt;

&lt;p&gt;A good build convention removes repeated decisions that are easy to get wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-module Gradle builds are where this pays off
&lt;/h2&gt;

&lt;p&gt;Single-module scanner integration is easy to demo.&lt;/p&gt;

&lt;p&gt;Multi-module builds are where the real problems appear.&lt;/p&gt;

&lt;p&gt;A root project may not contain production code. Some modules are deployable. Some are libraries. Some are test fixtures. Some should be excluded from coverage or dependency analysis. SonarQube needs paths per module. Dependency-Check may need aggregate behavior. SBOM generation should describe the deployable artifact.&lt;/p&gt;

&lt;p&gt;The plugin detects Java subprojects using &lt;code&gt;java&lt;/code&gt; and &lt;code&gt;java-library&lt;/code&gt;, supports module filters, configures coverage per Java module, collects XML paths, prepares root-level SonarQube analysis, and exposes a root-level security workflow.&lt;/p&gt;

&lt;p&gt;A root command can still drive the work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew clean securityAnalyze &lt;span class="nt"&gt;--no-daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between a scanner integration and a build tooling layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD becomes smaller
&lt;/h2&gt;

&lt;p&gt;Once the build owns the security wiring, CI/CD becomes easier to read.&lt;/p&gt;

&lt;p&gt;A GitLab job can be this boring:&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;security:gradle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;GRADLE_USER_HOME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_PROJECT_DIR/.gradle"&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew clean securityAnalyze --no-daemon&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&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;expire_in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7 days&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build/reports/dependency-check/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build/reports/cyclonedx/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build/reports/cyclonedx-direct/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/build/reports/jacoco/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SonarQube can stay separate and token-driven:&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;sonarqube:gradle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:17&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./gradlew sonar --no-daemon&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$SONAR_TOKEN'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is architectural:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI/CD calls build tasks.
CI/CD does not reimplement the build conventions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where pre-commit and Gitleaks fit
&lt;/h2&gt;

&lt;p&gt;I do not see the Gradle plugin as the first security layer.&lt;/p&gt;

&lt;p&gt;For secrets, I want something earlier.&lt;/p&gt;

&lt;p&gt;A pre-commit hook with Gitleaks can catch obvious secrets before a commit exists. That is the right place for that class of finding.&lt;/p&gt;

&lt;p&gt;The Gradle plugin covers the next layer: build-time AppSec checks that are heavier than a commit hook but still useful before code is pushed or reviewed.&lt;/p&gt;

&lt;p&gt;The layered model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;before commit
  Gitleaks + pre-commit

before review
  ./gradlew securityAnalyze

after push
  CI/CD runs the same build tasks and publishes artifacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps pre-commit fast, keeps Gradle responsible for build-aware checks, and keeps CI/CD responsible for enforcement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers get
&lt;/h2&gt;

&lt;p&gt;Developers get fewer mystery pipelines.&lt;/p&gt;

&lt;p&gt;They can run one command locally and get the same shape of output that CI/CD will collect later. They do not need to remember where Dependency-Check writes reports or which SBOM task matters for a multi-module Spring Boot project.&lt;/p&gt;

&lt;p&gt;They also get a workflow that respects their time. Fast checks happen in hooks. Heavier checks happen through the build. CI/CD verifies the result.&lt;/p&gt;

&lt;p&gt;That is a much better experience than discovering every AppSec issue after a push.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the security team gets
&lt;/h2&gt;

&lt;p&gt;The security team gets consistency.&lt;/p&gt;

&lt;p&gt;Reports arrive in predictable formats and paths. SonarQube analysis receives the metadata it needs. SBOM generation follows the same assumptions across services. Multi-module projects behave less like special snowflakes. CI/CD jobs become easier to audit because the security wiring is versioned in one build layer.&lt;/p&gt;

&lt;p&gt;Most importantly, the team gets a better adoption story.&lt;/p&gt;

&lt;p&gt;Instead of telling every service owner to copy another YAML block, AppSec can offer a build convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apply the plugin
set the project values
run securityAnalyze locally and in CI/CD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is easier to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;secure-build-gradle-plugin&lt;/code&gt; is not another scanner.&lt;/p&gt;

&lt;p&gt;It is a way to make existing scanners usable in normal Java engineering.&lt;/p&gt;

&lt;p&gt;The goal is not to move all security into Gradle. The goal is to put build-aware security checks in the build system, where developers can run them locally and CI/CD can reproduce them without copy-paste.&lt;/p&gt;

&lt;p&gt;That is the pattern I want across Secure SDLC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local where possible
build-aware where useful
CI/CD for enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that pattern works, security stops being a pile of pipeline scripts and becomes part of the engineering workflow.&lt;/p&gt;

&lt;p&gt;Project links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Niki-1337/secure-build-gradle-plugin" rel="noopener noreferrer"&gt;Secure Build Gradle Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Niki-1337/secure-build-maven-extension" rel="noopener noreferrer"&gt;Secure Build Maven Extension&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>cicd</category>
      <category>java</category>
      <category>gradle</category>
    </item>
  </channel>
</rss>
