SAST vs. Secret Scanning

Last updated: May 19, 2026


A hardcoded credential in your codebase can be detected by two fundamentally different techniques: static application security testing (SAST) and Secret Scanning. Both produce findings that may be summarized as "hardcoded secret" or similar, but they answer different questions, analyze different files, and protect against different categories of risk.

This article explains how Fluid Attacks classifies each, why both are needed, and what each looks like in practice.

The core distinction

DimensionSASTSecret Scanning
What triggers detectionThe code reveals that a value must be secret: a constant flow into PBEKeySpec, a KeyParameter, an HTTP client constructor, an authentication parameter. The destination in code is what matters.The value looks like a secret: it matches the format of an AWS Access Key, a GitHub token, a JWT, a PEM block, or has sufficiently high entropy. The shape of the string is what matters.
Scope of analysisExecutable source code only (Java, Scala, Python, Go, TypeScript, etc.). Program-flow analysis.Any file in the repository: source code, .env, YAML, JSON, Markdown, notebooks, even commit history. Content analysis.
Question it answersDid the developer write insecure code around a secret? The root issue is poor cryptographic or authentication practice; the secret itself is a symptom.Did a credential leak into the repository? The root issue is exposure; the surrounding code is irrelevant.

In one line: SAST catches secrets by their use. Secrets Scanning catches them by their shape.

SAST: detection by context

A SAST method fires when a value reaches a security-sensitive sink, regardless of what the value looks like. The literal "123" will trigger a SAST finding if it lands in the wrong place. A perfectly random 32-character string will not, if it sits inert in a comment.

Example 1 — Hardcoded password in PBEKeySpec (Java/Scala)

import javax.crypto.spec.PBEKeySpec

val keySpec = new PBEKeySpec("myPassword".toCharArray)

The string "myPassword" has no entropy, no recognizable format, and no prefix that any secrets scanner would flag. The finding exists because the value flows into a PBEKeySpec constructor — an API whose entire purpose is to consume a secret.

Example 2 — Hardcoded KeyParameter (Scala, Bouncy Castle)

import org.bouncycastle.crypto.params.KeyParameter

val key = new KeyParameter(Array[Byte](0x01, 0x02, 0x03, 0x04))

The byte array has no telltale pattern. The finding exists because KeyParameter is a cryptographic key holder; anything passed to it is, by definition, a key that should not be hardcoded.

Example 3 — Credentials in an SDK client constructor (Python)

import boto3

s3 = boto3.client(
    "s3",
    aws_access_key_id="placeholder",
    aws_secret_access_key="changeme",
)

The SAST finding is justified by the call siteboto3.client() accepting credentials as keyword arguments — not by the format of the strings. The same rule fires even when the values are obvious placeholders that no secrets scanner would catch.

Secret Scanning: detection by shape

A secret scanner fires when a string matches a known credential pattern, regardless of where it appears. It does not need to understand the surrounding language, framework, or call graph. It only needs the file to be readable as text.

Example 1 — AWS Access Key in .env

# .env
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

.env files are configuration, not source code; SAST engines do not parse them. But the AKIA prefix is a fixed identifier for AWS Access Key IDs and is unambiguous to a secrets scanner.

Example 2 — GitHub token in a code comment

# TODO: rotate this — ghp_aBcDeFgHiJkLmNoPqRsTuVwXyZ1234567890
def fetch_repo():
    ...

A SAST engine sees an inert comment with no program effect. A secret scanner sees the ghp_ prefix — the canonical marker for a GitHub Personal Access Token — and reports a leaked credential, even though the value is never used by the code.

Example 3 — Private key embedded in documentation

## Local setup

To connect to the staging cluster, paste this into `~/.ssh/staging_key`:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----

The file is documentation. It is never executed, never imported, never reachable from any program flow. SAST has no jurisdiction here. Secret Scanning does.

When both techniques fire on the same finding

There is a legitimate overlap zone. A real AWS access key passed directly to a client constructor will be detected by both engines:

boto3.client(
    "s3",
    aws_access_key_id="AKIAIOSFODNN7EXAMPLE",
    aws_secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
)
  • SAST reports: Hardcoded credential passed to AWS SDK client.
  • Secret Scanning reports: AWS Access Key ID pattern detected in source file.

This is not duplication. It is two independent confirmations of the same risk, arrived at through different reasoning. Each finding may carry distinct remediation guidance, severity, and ownership.

Why both techniques are necessary

The two are complementary and not interchangeable.

A SAST-only program misses secrets that live in .env files, infrastructure manifests, configuration, documentation, and commit history — because SAST never reads those files.

A Secret-Scanning-only program misses hardcoded credentials whose values are weak, placeholder-shaped, or otherwise lack a recognizable format — because pattern matching cannot infer that "changeme" is being used as a real key. It also misses cryptographic misuse where the practice is wrong even when the value is, technically, just a string (e.g., a hardcoded IV, a hardcoded salt, a hardcoded HMAC secret without an obvious prefix).

When evaluating coverage, the right question is not "does the platform find hardcoded secrets?" — it is "does the platform find secrets through use and through shape?" Fluid Attacks runs both, and each engine reports findings under its own technique so that the root cause and remediation path remain unambiguous.

On this page