Pass 06: Twelve-Factor Readiness (TF)
| Property | Value |
|---|---|
| Pass number | 06 |
| Key | tf |
| Pass name in output | twelve_factor |
| Signal prefix | TF (IaC scanner findings: TF-IAC) |
| Type | Deterministic (rule engine), optional external IaC scanner, no LLM |
| Default | On |
| Assessment types | application; llm (inside each leg). Skipped for service (no Terraform or twelve-factor source). Not run for landing-zone-catalog. |
| Tier | Not tier-gated (Community and above) |
What it is for
The Twelve-Factor App method describes properties that make an application easy to run on modern cloud platforms: configuration in the environment, explicit dependencies, horizontal scaling, logs as event streams, fast start-up and shutdown. Pass 06 answers: "How cloud-ready is this application, and what would block a replatforming?"
It checks five of the twelve factors that can be judged from the repository, and, when Terraform files are present and a scanner is installed, runs a static IaC security scan.
What you need to provide
- The application source tree.
- Optional: Terraform files (
*.tf) anywhere in the source tree (for example ininfra/orterraform/), together with Checkov or KICS on thePATH, to get IaC security findings.
No LLM is needed. The pass reads the language detected by Pass 01 (INV) from the same run, so run both together for the best result.
How it works
The pass searches up to three directory levels for .env.example and for Compose files (docker-compose*.yml, compose*.yml), and checks the root for k8s/ and helm/.
| Factor | Check | Status values |
|---|---|---|
| III Config | Stack-aware externalisation evidence and hard-coded values in configuration (see below) | pass, partial, fail, unknown |
| II Dependencies | The Compose file references public Docker Hub (docker.io, hub.docker.com) or bare images postgres, redis, node, nginx, ubuntu | pass (Compose without public references), partial (public registry), unknown (no Compose) |
| VIII Concurrency | Kubernetes manifests present | pass (Kubernetes), fail (Compose only), unknown |
| XI Logs | Node.js: pino, winston, bunyan, nestjs-pino or pino-http in package.json. Rust: tracing, log, env_logger, tracing-subscriber or slog in Cargo.toml. Other languages are not checked. | pass, partial, unknown |
| IX Disposability | A health handler file (health.controller.ts, health.rs, healthcheck.rs, health_check.rs, up to four levels) or a src/health or apps/api/src/app/health directory | partial (health endpoint found; graceful shutdown cannot be confirmed statically) or unknown |
Factor III by stack. The pass recognises the configuration idiom of each stack instead of looking for .env.example only:
- Spring Boot:
${ENV}placeholders inapplication*.yml/.properties,spring.config.import, Spring Cloud Config or Vault,@Value("${...}")and@ConfigurationPropertiesin Java or Kotlin code; profile files count as supporting evidence. - Helm and Kubernetes: chart templates reading
.Values,ConfigMapandSecretmanifests,envFrom,configMapKeyRefandsecretKeyRef; per-environmentvalues-<env>.yamlfiles count as supporting evidence. - Node.js:
.env.example(also.env.sample,.env.template),process.envreads (notNODE_ENValone),dotenv. - Python:
os.environ,os.getenv, pydanticBaseSettings. .NET:AddEnvironmentVariables(), environment-specificappsettings.<Environment>.json.
Hard-coded values count against Factor III: a public or private host or IP address (found by the shared endpoint scanner) in packaged configuration (application*.yml, appsettings*.json), and a literal password, secret, token or API key in packaged configuration, Helm values or ConfigMaps. The value is never shown, only the key and file:line. A host inside a placeholder (${DB_HOST}) counts as externalised. A host in a per-environment Helm values file (values-<env>.yaml) is environment configuration and is listed for information only; a host in the chart default values.yaml or in a ConfigMap fixes one environment in the artefact and lowers TF-01 to low (Factor III partial). A cluster service name (postgres, *.svc.cluster.local) is resolved by the platform and is listed for information only. Test resources (src/test/resources, application-test.yml) are ignored.
IaC scan. The pass looks for Terraform roots below the source root: the top-most folders, up to six levels deep, that contain .tf files (at most ten roots; hidden folders such as .terraform and .git, and node_modules, vendor, dist, build and target are skipped). A folder below a root that was already found is not listed again, because the scanners recurse. For each root the pass runs checkov --directory <root> -o json --compact --quiet. If Checkov is not installed or prints nothing, it tries kics scan --path <root> --report-formats json --output-path - --no-progress. Each tool gets 60 seconds. Findings with the same rule ID and resource are reported once. Failed checks become TF-IAC-NN signals.
How it is measured
| Signal | Emitted when | Severity | Meaning |
|---|---|---|---|
TF-01 | Always | high (hard-coded endpoint or credential, with file:line), positive (externalisation evidence for the stack), low (only supporting evidence, or literal host defaults inside placeholders), medium with confidence low (not determinable: no evidence either way) | Factor III: configuration externalised, hard-coded, or not determinable |
TF-02 | Always | medium (public registry), positive (Compose without public images), low (no Compose) | Factor II: images must be mirrored to a sovereign container registry |
TF-03 | Always | high (Compose without Kubernetes), informational otherwise | Factor VIII: no multi-instance deployment, a DORA operational resilience gap |
TF-04 | Always | positive (structured logging), medium (none found), informational (language not checked) | Factor XI: structured JSON logs needed for SIEM integration |
TF-05 | Always | low | Factor IX: health endpoint found or not; shutdown hooks not confirmed |
TF-IAC-01 onwards | One per failed IaC check | high (scanner severity critical or high), medium otherwise | Rule ID, message and resource from Checkov or KICS |
Score. twelve_factor_pass_rate = number of factors with status pass divided by the number of factors whose status is not unknown, rounded to two decimals (0 when nothing could be checked). Because Factor IX can only be partial or unknown, the highest reachable rate when all five factors are assessed is 0.8. IaC findings do not affect the rate.
The assessment block records factor_scores (factor, status, note), twelve_factor_pass_rate, iac_roots (number of top-level Terraform / OpenTofu folders) and config_externalisation (Factor III status, confidence, recognised stacks, positive and supporting evidence, hard-coded values and placeholder defaults with file:line, deployment literals, number of files checked). When Factor III cannot be determined it is unknown and therefore does not lower the rate.
How results feed later passes. The cloud-native score (#3431) is computed from factor_scores: the weighted mean with pass 1, partial 0.5, fail 0 and unknown 0.5 (unknown counts half instead of being left out, unlike twelve_factor_pass_rate), reported with factors_checked and a confidence; see Pass 09 (SYNTH). iac_roots and the Helm and Kubernetes stacks are infrastructure-as-code and container evidence for the portability score. TF-* signals are evidence for Pass 11 (COMP).
Third-party modules and tools
| Name | Purpose in this pass | Licence | Bundled or external | When missing |
|---|---|---|---|---|
| Checkov | Static IaC security scan of .tf files (tried first) | External tool; licence not recorded in repo | Optional external install on PATH | KICS is tried |
| KICS | Static IaC security scan (fallback) | External tool; licence not recorded in repo | Optional external install on PATH | No TF-IAC signals; the factor checks run normally |
| js-yaml | Reading 01-inv.yaml from the current run | MIT | Bundled | Not applicable |
@swao/module-iac-scan (SWAO internal) | Wrapper that invokes Checkov or KICS and normalises results | SWAO internal package | Bundled | Not applicable |
swao health-check does not currently probe for Checkov or KICS; check with checkov --version or kics version yourself.
Outputs
wsp/runs/<ts>/passes/06-tf.yamlwith header (id: 6,name: twelve_factor), signals andassessment.- Portfolio event log entries
pass.iac.scanner.startandpass.iac.scanner.completewhen a scan runs.
Configuration and limitations
Configuration
--passes tfruns the pass alone (includeinvfor language-aware logging checks).- Install Checkov or KICS on the
PATHto enableTF-IACfindings.
Limitations and known gaps
- Only five factors are assessed (III, II, VIII, XI, IX). The other seven factors are not evaluated.
- The IaC scan covers at most ten Terraform roots up to six folder levels below the source root.
- When neither scanner is installed, no message tells you that the scan was skipped.
- The public registry check recognises only Docker Hub references and five common bare image names.
- Factor XI is checked for Node.js, Rust and Java (including Spring Boot) only.
- TF-01 does not use the Gitleaks findings of Pass 14 (secret scan): Pass 14 runs after this pass. Credentials in configuration are matched by key name (or as an inline password in a connection string) only, so a secret under an unusual key (
app.integration.value) does not lower Factor III. - Every public or private host or IP address in packaged configuration counts as hard-coded, including well-known public SaaS endpoints such as an OAuth or OpenID Connect issuer (
spring.security.oauth2.resourceserver.jwt.issuer-uri). There is no allowlist for such endpoints yet; review a TF-01 high finding that only cites a public identity provider before acting on it. - Configuration files of the Node.js
configpackage (config/default.json,config/production.json,config/custom-environment-variables.json) are not read. For Node.js services onlyprocess.envreads, the dotenv loader and.env.examplecount as evidence, and literal hosts in those JSON files are not reported.
