A team that runs only SAST in CI and considers itself “secure” is checking one corner of a much larger surface. SAST catches the patterns you wrote. It misses runtime configuration drift, secrets accidentally committed, deprecated cipher suites left enabled at the edge, ports unintentionally exposed at the origin. To go past the CI baseline we layered four additional security tools on top of what already runs on every MR. One of them caught a real production credential committed to the repository, and the fix shipped the same day. This blog walks through the layered audit, what each tool found, and how the tooling stack matches up against OWASP Top 10 categories.

Note: Our project is hosted on an internal GitLab instance, so we use the term MR (Merge Request) throughout this blog. If you’re coming from GitHub, MRs are the equivalent of Pull Requests (PRs).

What We Already Have in CI

Two security checks run automatically on every MR via the security:sast GitLab CI job:

ToolLayerWhat it catches
BanditPython SASTeval(user_input), weak crypto, hardcoded secrets, unsafe deserialisation
pnpm auditnpm dependency scanKnown CVEs in npm dependencies and transitive ones

The job uses a tiered severity policy: HIGH bandit or critical/high npm fails the build (blocks merge); MEDIUM/moderate is a soft warning; everything else is green. This setup has caught real issues: most notably the axios GHSA-3p68-rc4w-qgx5 SSRF advisory, blocked at MR time before the vulnerable version could merge.

But CI checks like these have structural blind spots. They cannot probe the running system, they cannot scan for secrets in git history, they cannot enumerate cipher suites at the CDN edge, they cannot map open ports on the origin VM. To cover those, you have to run different categories of tool.

Why Four More Tools

Each of the four manual scans below targets a specific gap that CI SAST plus dependency audit cannot reach:

ToolLayerThe gap it closes
NucleiDAST against live systemConfiguration drift visible only at runtime (exposed admin panels, weak ciphers, default error pages)
testssl.shDeep TLS analysisCipher suites, certificate chain, forward secrecy, OCSP stapling — far past what generic DAST templates check
gitleaksSecret scanning across git historyAPI keys, tokens, passwords accidentally committed, including in old commits
nmapNetwork port mapping at the originWhat is actually exposed on the VM behind the CDN, which Cloudflare hides from public scans

The combination of two automated CI tools plus four manual scans gives genuine layered defence. Each tool answers a different question; the union of their outputs is much more informative than any individual scan.

Tool 1 of 4: Nuclei DAST

Nuclei is a CLI-driven dynamic application scanner with 6,500+ community-maintained YAML templates. It probes a live system with real HTTP requests and matches responses against known vulnerable patterns.

Tool choice over alternatives

ToolSpeedStrengthsWeaknesses
Nuclei~4 min for 6,537 templates against 2 hostsFast, JSON output, version-controlled templatesTemplate-driven, no novel logic discovery
OWASP ZAPHoursActive spider + fuzzer, intercepting proxyGUI-heavy, harder to script in CI
MetasploitManualExploit frameworkGeared to post-exploitation, not baseline auditing

Nuclei wins on speed and CI-friendliness. Metasploit is the tool the IR rubric explicitly mentions as an example, but its strength is exploitation after a vulnerability is found, not finding the vulnerability in the first place. For a baseline audit, Nuclei is the right primary.

Running the scan

nuclei -update-templates  # one-time, then weekly
nuclei \
  -u https://sira.nashtagroup.co.id \
  -u https://sira-api.nashtagroup.co.id \
  -severity low,medium,high,critical \
  -jsonl -o /tmp/sira-nuclei-scan/findings.jsonl \
  -stats -stats-interval 10

The -severity low,medium,high,critical flag filters out informational findings (server fingerprints) so the output is actionable signal only.

Result: 4 findings, all the same template, all severity LOW

[INF] Templates loaded for current scan: 6537
[INF] Targets loaded for current scan: 2
[INF] Templates clustered: 454 (Reduced 780 Requests)
[INF] Scan completed in 3m. 4 matches found.
TemplateSeverityMatcherHostCipher
ssl/weak-cipher-suites.yamlLowtls-1.0sira.nashtagroup.co.idTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
ssl/weak-cipher-suites.yamlLowtls-1.1sira.nashtagroup.co.idTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
ssl/weak-cipher-suites.yamlLowtls-1.0sira-api.nashtagroup.co.idTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
ssl/weak-cipher-suites.yamlLowtls-1.1sira-api.nashtagroup.co.idTLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA

This maps to OWASP A02:2021 Cryptographic Failures. Both hosts resolve to Cloudflare edge IPs (104.21.26.254 and 172.67.139.195), which means the TLS handshake happens at Cloudflare, not on our origin. Cloudflare is offering a TLS profile that still includes TLS 1.0 and TLS 1.1 as fallback options. Modern browsers will negotiate TLS 1.2 or 1.3, but the legacy versions are still negotiable for downgraded clients.

What Nuclei did NOT find (positive evidence)

The 6,537 templates also probed for the rest of the OWASP Top 10. Zero matches across:

OWASP categoryResult
A01 Broken Access ControlNo matches (no exposed admin panels, no default credentials)
A03 InjectionNo matches (SQLi, XSS, command injection probes)
A04 Insecure DesignNo matches
A05 Security MisconfigurationNo matches (no exposed .env, .git, wp-admin, debug pages)
A06 Vulnerable ComponentsNo matches (no fingerprintable vulnerable framework versions)
A07 Auth FailuresNo matches
A08 Software/Data IntegrityNo matches
A09 Logging FailuresNo matches (no exposed log files)
A10 SSRFNo matches

Absence of findings is itself evidence: the existing controls (Cloudflare in front, no exposed dev artifacts, conservative framework defaults) are working. The only gap Nuclei found is the one TLS misconfiguration above.

The fix path

The fix is one Cloudflare zone setting:

Cloudflare Dashboard > nashtagroup.co.id > SSL/TLS > Edge Certificates
> Minimum TLS Version > set to 1.2

We don’t have admin access to the nashtagroup.co.id Cloudflare zone (owned by NashtaGroup, the project’s industry partner). After thinking through what asking would actually require, we decided not to pursue this fix at the CDN layer, for two reasons:

  1. Zone-wide scope. The Cloudflare “Minimum TLS Version” setting applies to every subdomain on nashtagroup.co.id, not just the three SIRA hostnames. Any other NashtaGroup service running on that zone (which we don’t fully visibility into) could potentially break for legacy clients we don’t know about. The scoped alternative (Configuration Rules) would limit the change to SIRA subdomains only, but even that is asking another team to make a change in their tooling for a finding that practically affects no real user (modern browsers all negotiate TLS 1.2 or 1.3 by default).
  2. Practical exploitability is low. This is a LOW-severity finding. It requires an active MitM with cipher-suite manipulation AND exploitation of CBC weaknesses (BEAST, POODLE) to extract anything. Real production user impact is essentially zero given that no one in our user base is on a TLS-1.0-era client.

The right call here is document the finding, recommend the fix, do not push for it. Logged in this blog as a known LOW-severity finding scoped to the layer of infrastructure we don’t own.

This is a useful reminder that security findings have ownership boundaries. Some findings the application engineer can fix unilaterally (the gitleaks finding below is a perfect example, fix shipped same day). Others require coordination with another team, and that coordination has a real cost (relationship capital, their queue time, their risk model). Part of doing security work in a real organisation is judging which findings are worth pushing for and which to leave as documented recommendations. Treating every finding as “must fix immediately, file ticket against whoever owns the layer” is how application engineers burn out their relationship with platform teams.

Tool 2 of 4: testssl.sh

Nuclei’s TLS template is shallow: it detects that legacy versions are negotiable, but it does not enumerate which cipher suites are actually offered, whether forward secrecy is configured, what signature algorithms the certificate supports, or whether OCSP stapling is enabled. For deep TLS analysis, the right tool is testssl.sh.

Running the scan

testssl.sh \
  --jsonfile /tmp/sira-security-r2/testssl-web.json \
  https://sira.nashtagroup.co.id

testssl.sh \
  --jsonfile /tmp/sira-security-r2/testssl-api.json \
  https://sira-api.nashtagroup.co.id

Each scan takes about 5-7 minutes and exercises hundreds of cipher and protocol checks against the live endpoint.

What testssl confirmed and what it added

testssl ran 378 individual checks per host. Headline findings, grouped by severity:

MEDIUM severity (5 distinct, both hosts):

Finding IDDetailWhat it means
overall_gradeBOverall TLS posture grade. Cannot reach A while TLS 1.0/1.1 are offered.
cert_expirationStatusexpires < 60 days (41)Certificate expires on 2026-06-07 (41 days from this scan). Cloudflare manages auto-renewal but worth confirming the renewal will trigger.
cert_notAfter2026-06-07 17:18Exact expiration.
BEAST_CBC_TLS1ECDHE-ECDSA-AES128-SHA ECDHE-ECDSA-AES256-SHABEAST attack: CBC ciphers in TLS 1.0 are theoretically downgrade-attackable. Same root cause as the Nuclei finding (TLS 1.0 enabled), but testssl names the specific ciphers.
security_headers (web only)missingCSP, HSTS, X-Frame-Options, etc. not set at edge. Cloudflare can inject these.
BREACH (web only)possibleBREACH attack: HTTPS compression on responses with secrets. Often a false positive for read-only public pages, worth investigating per-endpoint.

LOW severity (concentrated on cipher details):

  • TLS1, TLS1_1 — confirms Nuclei finding that legacy versions are negotiable.
  • cipherlist_OBSOLETED — obsolete cipher list still offered.
  • cipher-tls1_xc009, cipher-tls1_xc00a, plus several cipher-tls1_1_* and cipher-tls1_2_* — specific cipher suite IDs flagged (each one is a CBC variant testssl prefers be removed).
  • LUCKY13 — Lucky13 timing attack on CBC ciphers. Related to TLS 1.0/1.1 enablement.
  • cert_trust_wildcard — wildcard certificate trust note (informational about how trust resolves).
  • DNS_CAArecord — no CAA DNS record published (recommendation to add CAA records pinning the issuing CA).
  • FS_TLS12_sig_algs — note about TLS 1.2 signature algorithms.

What’s actually GOOD (positive evidence from testssl):

  • TLS 1.2 and TLS 1.3 are both offered (TLS 1.3 with modern TLS_AES_256_GCM_SHA384 and TLS_CHACHA20_POLY1305_SHA256).
  • Robust forward secrecy (multiple ECDHE cipher suites).
  • Server cipher order preference: yes (server picks strong ciphers first).
  • Post-quantum hybrid key exchange: X25519MLKEM768, X25519Kyber768Draft00 (genuinely cutting-edge, ahead of most production deploys).
  • Elliptic curves: prime256v1, secp384r1, secp521r1, X25519.
  • Signature algorithms: ECDSA with SHA-256/384/512 (no MD5, no SHA-1).
  • No Heartbleed, no POODLE-direct, no CRIME, no FREAK, no LOGJAM.

The picture testssl draws is “the modern half of the configuration is excellent (post-quantum-ready, in fact); the legacy halves are simply still left on as fallback.” The Cloudflare TLS 1.2 minimum fix will turn off the legacy halves without affecting any of the modern setup. Once that lands, the overall grade should jump from B to A and most of the LOW-severity cipher findings disappear with it.

This is exactly the depth Nuclei alone cannot produce. Nuclei reported “TLS 1.0/1.1 enabled” and that was it. testssl reports the specific cipher IDs, the related attacks (BEAST, Lucky13), the certificate expiration window, the missing security headers at the edge, and the post-quantum readiness of the modern half. Running both tools together is what produces a defensible picture.

The certificate expiration finding (41 days) is itself a useful catch that none of the other tools surfaced. Cloudflare’s auto-renewal should handle it before expiry, but it goes on the watchlist as something to verify if the renewal hasn’t fired by mid-May.

Tool 3 of 4: gitleaks (Real Finding, Same-Day Fix)

This tool produced the highest-impact finding of the entire audit. Worth walking through end-to-end because it satisfies the rubric’s level-4 criterion (“show before/after of code improved based on screening tool results”) with a real, shipped fix.

What gitleaks does

Gitleaks scans a git repository (current files plus history) for patterns that match known secret formats: AWS keys, Stripe tokens, generic API keys, JWT-shaped strings, OAuth tokens, etc. It is the same class of tool as truffleHog and OSV-scanner-secrets but standalone and fast.

Initial scan: 474 raw findings

gitleaks detect \
  --source "/path/to/sira" \
  --report-path /tmp/sira-security-r2/gitleaks.json

Output: 474 leaks found across 1,630 commits scanned in 1 minute 5 seconds.

A naïve reading of this number is panic. The honest reading requires triage.

Triage: 473 false positives, 1 real

Gitleaks counts each occurrence in each commit. A single committed secret that survives 50 commits gets counted 50 times. To find the real signal, you have to group by file and rule:

jq -r '.[] | "\(.RuleID) | \(.File):\(.StartLine)"' /tmp/sira-security-r2/gitleaks.json \
  | sort | uniq -c | sort -rn | head

Top categories after grouping:

CountTypeLocationVerdict
75generic-api-key.env.example:26False positive (placeholder values like sonarqube_token_placeholder)
74generic-api-key.cursor/mcp.json:33REAL LEAK (production SonarQube token, see below)
73stripe-access-tokenCLAUDE.md:43-44False positive (documented Stripe test key example, sk_test_4eC39HqLyjWDarjtT657)
64generic-api-key.mcp.json:26REAL LEAK (same SonarQube token)
11generic-api-keyapps/api/conftest.py:12False positive (test fixture with mock keys)
88curl-auth-user.gitlab-ci.yml, .claude/commands/sonarqube.mdFalse positive (${VAR} env-var references, not literals)

The methodology that matters: never trust raw scanner counts; group by file and rule first. Most secret-scanner output is dominated by false positives in placeholder/test/example files. The real findings are usually concentrated in 1-3 files.

The real leak

Hidden inside the 474 noise was one genuine secret committed to the repo:

// .mcp.json (and .cursor/mcp.json, identical content)
"sonarqube": {
  "command": "docker",
  "args": [...],
  "env": {
    "SONARQUBE_TOKEN": "squ_9a69847c8626572c448b8a4001033b2708d15211",
    "SONARQUBE_URL": "https://sonarqube.cs.ui.ac.id"
  }
}

This is a production user token to the project’s SonarQube instance (sonarqube.cs.ui.ac.id), hardcoded across two MCP server config files plus referenced in a slash-command markdown. Anyone with read access to the repo could:

  • Read all SonarQube data for the SIRA project
  • Submit fake scan results to corrupt quality-gate decisions
  • Pivot to other projects on the same SonarQube instance if the token had broader scope

Because the repo is hosted on a private internal GitLab and the SonarQube instance is on the CS-UI internal network (not public internet), the practical exposure was limited to the 5-person team plus GitLab admins. But the principle is unchanged: a production credential was in version control where it did not belong.

Same-day fix shipped

The remediation shipped within an hour of detection:

  1. Refactored .mcp.json and .cursor/mcp.json to remove the sonarqube MCP entry entirely. The MCP server was redundant with our existing /sonarqube slash command which calls the SonarQube REST API directly.
  2. Refactored the /sonarqube slash command to source the token from .env.local (gitignored) instead of hardcoding it in the markdown:
# Load from .env.local
set -a
source .env.local
set +a

# Verify both vars are set; abort if not
if [ -z "${SONARQUBE_TOKEN:-}" ] || [ -z "${SONARQUBE_URL:-}" ]; then
  echo "ERROR: SONARQUBE_TOKEN or SONARQUBE_URL missing from .env.local" >&2
  exit 1
fi
  1. Verified .env.local was gitignored (it was, line 2 of .gitignore) and added the token there with a rotation warning comment.
  2. Updated .env.example so future contributors know to configure their own tokens locally.
  3. Rotated the leaked token in the SonarQube UI (Account → Security → Revoke + regenerate). The new token replaces the leaked one in .env.local. Even if someone clones the historical commit and reads the leaked value, the token is now invalid.
  4. Committed and pushed to main (88697bdd).

Re-scan after the fix

gitleaks detect \
  --source "/path/to/sira" \
  --report-path /tmp/sira-security-r2/gitleaks-after.json \
  --no-git

Result: 21 findings, all confirmed false positives (test fixtures, env-var references in YAML/markdown, the documented Stripe test key). The leaked token is gone from the current working tree:

# Search for any remaining occurrence of the leaked token
jq -r '.[] | select(.Secret | test("squ_9"))' gitleaks-after.json
# (no output)

This is the level-4 evidence the rubric asks for: a real screening tool found a real issue, the issue was fixed and verified by the same screening tool, with measurable before-and-after counts.

Why the count dropped from 474 to 21

The first scan included git history (1,630 commits, ~525 MB scanned). The second scan was current-files-only (--no-git). Two effects:

  1. History exclusion: the leaked token still exists in old commits forever (rewriting git history would be required to fully purge, which has team-coordination costs). Rotating the token at the source is the practical mitigation.
  2. Lower per-file count: each file now reports its current findings once instead of once-per-commit-where-it-existed.

The real success metric is the leaked token no longer matches any current file, confirmed by the explicit jq query above.

Tool 4 of 4: nmap (Origin VM Verification)

Nuclei scans the public-facing hostname, which on Cloudflare-fronted apps means Cloudflare’s edge IPs, not the actual origin VM. To verify what is exposed at the origin, you need to scan the VM directly. We are on the same Tailscale tailnet as the Nashta VM (10.10.25.75), which makes this trivial.

Why this matters

A common misconfiguration pattern: a developer adds a quick debug HTTP endpoint to the origin VM, forgets to firewall it, and Cloudflare proxies the documented public ports while the debug port stays open to the internal network. Anyone on the same network (Tailscale tailnet, internal LAN, VPN) can hit it directly.

Running the scan

nmap -sV -Pn 10.10.25.75

-sV enables service version detection; -Pn skips the host-up check (the VM blocks ICMP).

Result: clean origin profile

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    nginx 1.29.8
9999/tcp open  http    Uvicorn

Service detection performed in 10.35 seconds.

Three open ports out of 1,000 scanned, all expected:

PortServiceWhy it’s open
22OpenSSH 9.6p1Admin SSH access for the team
80nginx 1.29.8Reverse proxy receiving traffic from Cloudflare Tunnel (Cloudflare terminates TLS at the edge and forwards via HTTP to origin)
9999UvicornFastAPI server, proxied behind nginx

One observation worth flagging

Port 9999 (Uvicorn) is bound to 0.0.0.0:9999, which means anyone on the Tailscale tailnet can reach http://10.10.25.75:9999/api/... directly without going through nginx. The tailnet is a trusted network (only the team’s devices), so the practical exposure is limited, but defence in depth would suggest binding Uvicorn to 127.0.0.1:9999 so only the local nginx process can reach it. This is a backlog item, not a critical fix.

What nmap didn’t find is the more important signal:

  • No port 443 directly on the VM (TLS terminates at Cloudflare, as designed)
  • No exposed database ports (Postgres on 5432, Redis on 6379 are local-only)
  • No exposed admin panels (no Grafana, Prometheus, RabbitMQ management UI on common ports)
  • No development services left running (no Vite dev server on 5173, no Storybook on 6006)

A clean origin port profile is exactly what you want to see. Nmap confirms that the deployment pipeline is not accidentally exposing anything past the documented public ports.

How the Six Layers Compose

The audit + CI now covers six tool layers:

LayerToolTriggerWhat it catches
1Bandit (Python SAST)Every MRCode patterns: weak crypto, eval, hardcoded secrets at write time
2pnpm audit (npm deps)Every MRKnown CVEs in npm dependencies and transitives
3Nuclei (DAST)Manual / scheduledRuntime configuration: weak ciphers, exposed admin panels, default error pages
4testssl.sh (TLS deep)Manual / scheduledCipher suites, FS, OCSP, certificate chain, post-quantum readiness
5gitleaks (secrets)Manual / pre-commit hookAPI keys, tokens, passwords accidentally committed
6nmap (origin port)Manual / scheduledWhat is actually open on the VM behind the CDN

Each layer answers a question the others cannot:

  • SAST (1) cannot find a secret committed to a JSON config file. Gitleaks (5) catches it.
  • Dependency audit (2) cannot find a TLS misconfiguration at the edge. Nuclei (3) and testssl (4) catch it.
  • DAST (3) cannot enumerate cipher suites in detail. testssl (4) does.
  • Public DAST (3) cannot see what is exposed behind the CDN. nmap (6) against the origin can.
  • None of layers 1-4 can find a debug port left open on the VM. nmap (6) can.

A team that runs only one or two of these is missing categories of risk it does not even know about. The real value of the layered audit is that the union of findings is actionable, while the union of non-findings is genuine evidence the system is broadly secure.

Round Plan and Score Arc

flowchart LR
    A([Round 1: Baseline scan]) --> B[Layered audit:
Nuclei + testssl + gitleaks + nmap] B --> C[Real finding:
SonarQube token leak] C --> D[Fix shipped same day:
commit 88697bdd] D --> E[Round 1.5 - this blog:
before/after evidence ready] E --> F[Round 2: Wire scans into CI
scheduled weekly job] F --> G([Continuous DAST + secret scan
as sustained guardrail]) B --> H[TLS 1.0/1.1 finding:
logged as recommendation,
not pursued cross-team]

The score evidence as it stands:

RoundEvidence shippedRubric mapping
1.0 (initial blog)Nuclei baseline, fix path identifiedLevel 3 (tool used, findings documented, fix path identified)
1.5 (this update)Layered 6-tool audit + real gitleaks finding + same-day fix verified by re-scanLevel 4 (before/after of code improved based on screening tool results, verified by re-scan)
2 (next iteration)One or more tools wired into CI as scheduled job (gitleaks pre-commit + Nuclei weekly)Level 4 sustained (continuous monitoring, not point-in-time)
(deprioritised)TLS 1.0/1.1 fix at Cloudflare edgeLogged as recommendation, not pursued because zone-scope spans services we don’t own

The gitleaks before/after is what specifically meets the rubric’s level-4 wording:

“Show before/after of code improved based on screening tool results.”

Before: scanner finds 1 real leak (SonarQube token in .mcp.json and .cursor/mcp.json). Fix: refactor to env-var, rotate token at source, commit 88697bdd. After: same scanner re-run on current files shows the leak is gone, all remaining findings confirmed false positives. The token rotation makes the historical leak useless even where it persists in git history.

What Each Tool Doesn’t Catch

The same anti-overclaiming discipline as in the testing-design and monitoring blogs. Six tools is more than zero, but six tools is not “complete coverage.” Honest scope:

Nuclei does not catch:

  • Logic-level bugs (authorisation checks that allow privilege escalation)
  • Bugs behind authentication (we ran unauthenticated; an authenticated scan is a follow-up)
  • Zero-days in our framework or CDN

testssl does not catch:

  • Application-layer crypto bugs (weak password hashing, predictable JWT secrets)
  • Anything past the TLS handshake

gitleaks does not catch:

  • Secrets stored in encrypted form in the repo (these would be in an encrypted blob, not a recognisable pattern)
  • Secrets in deploy-time-only files (CI environment variables, secret managers)
  • Logic bugs that expose otherwise-safe secrets at runtime

nmap does not catch:

  • Application-layer vulnerabilities on the open ports
  • Services configured to bind to non-standard ports outside the default 1000
  • Anything on networks we did not scan

The combination still leaves real gaps. Bugs that require multiple contexts simultaneously (a slow endpoint with a vulnerable dependency that violates the spec under load) will not be cleanly attributed to any single tool. Manual penetration testing, threat modelling, and production observability all sit alongside this audit. The goal of the layered baseline is to make the next bug as cheap as possible to find, not to claim invulnerability.

Reflection

Three things this exercise crystallised about real-world security work:

1. Real findings often hide in scanner noise. Gitleaks reported 474 raw findings on first scan. The naïve response is panic; the correct response is triage. Grouping by file and rule reveals that 473 are placeholders, env-var references, or test fixtures. The remaining 1 is a real production credential. Skipping the triage step would either waste hours fixing nothing or skip the one finding that actually matters.

2. Same-day fixes are the strongest possible level-4 evidence. The IR rubric asks for “before/after of code improved based on screening tool results.” That phrasing assumes a process: scan, find, fix, re-scan, document. The gitleaks SonarQube finding produced exactly this process within an hour of detection: scanner found the leak, refactored the configs, rotated the token, re-scanned, leak is gone. That is a complete cycle on something the application engineer can fix unilaterally.

3. Tools change the cost curve, not the underlying work. Without gitleaks, finding the leaked SonarQube token would have required reading every committed file by hand, hoping to spot a pattern. With gitleaks it took 65 seconds and produced the file paths and line numbers. The hours saved on detection got reinvested in the work that does not automate: triaging the noise, deciding fix priority, refactoring the configs to use env vars, coordinating the token rotation, updating the team’s slash command, and writing this blog. The right shape for security tooling is to automate the search and free human attention for the judgment.

Round 2 of this audit will wire one or more of these tools (most likely gitleaks as a pre-commit hook plus Nuclei on a weekly schedule) into the CI pipeline so the baseline becomes a sustained guardrail rather than a point-in-time snapshot. The layered story stays the same: SAST + dependency audit + DAST + deep TLS + secret scan + port scan, each one closing a gap the others structurally cannot. The TLS 1.0/1.1 finding stays on the recommendations list, available for whoever inherits the project to push for if their relationship with the CDN team allows it.