Skip to content

Pass 06: Twelve-Factor Readiness (TF) ​

PropertyValue
Pass number06
Keytf
Pass name in outputtwelve_factor
Signal prefixTF (IaC scanner findings: TF-IAC)
TypeDeterministic (rule engine), optional external IaC scanner, no LLM
DefaultOn
Assessment typesapplication; llm (inside each leg). Skipped for service (no Terraform or twelve-factor source). Not run for landing-zone-catalog.
TierNot 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 in infra/ or terraform/), together with Checkov or KICS on the PATH, 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/.

FactorCheckStatus values
III ConfigStack-aware externalisation evidence and hard-coded values in configuration (see below)pass, partial, fail, unknown
II DependenciesThe Compose file references public Docker Hub (docker.io, hub.docker.com) or bare images postgres, redis, node, nginx, ubuntupass (Compose without public references), partial (public registry), unknown (no Compose)
VIII ConcurrencyKubernetes manifests presentpass (Kubernetes), fail (Compose only), unknown
XI LogsNode.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 DisposabilityA 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 directorypartial (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 in application*.yml / .properties, spring.config.import, Spring Cloud Config or Vault, @Value("${...}") and @ConfigurationProperties in Java or Kotlin code; profile files count as supporting evidence.
  • Helm and Kubernetes: chart templates reading .Values, ConfigMap and Secret manifests, envFrom, configMapKeyRef and secretKeyRef; per-environment values-<env>.yaml files count as supporting evidence.
  • Node.js: .env.example (also .env.sample, .env.template), process.env reads (not NODE_ENV alone), dotenv.
  • Python: os.environ, os.getenv, pydantic BaseSettings. .NET: AddEnvironmentVariables(), environment-specific appsettings.<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 ​

SignalEmitted whenSeverityMeaning
TF-01Alwayshigh (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-02Alwaysmedium (public registry), positive (Compose without public images), low (no Compose)Factor II: images must be mirrored to a sovereign container registry
TF-03Alwayshigh (Compose without Kubernetes), informational otherwiseFactor VIII: no multi-instance deployment, a DORA operational resilience gap
TF-04Alwayspositive (structured logging), medium (none found), informational (language not checked)Factor XI: structured JSON logs needed for SIEM integration
TF-05AlwayslowFactor IX: health endpoint found or not; shutdown hooks not confirmed
TF-IAC-01 onwardsOne per failed IaC checkhigh (scanner severity critical or high), medium otherwiseRule 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 ​

NamePurpose in this passLicenceBundled or externalWhen missing
CheckovStatic IaC security scan of .tf files (tried first)External tool; licence not recorded in repoOptional external install on PATHKICS is tried
KICSStatic IaC security scan (fallback)External tool; licence not recorded in repoOptional external install on PATHNo TF-IAC signals; the factor checks run normally
js-yamlReading 01-inv.yaml from the current runMITBundledNot applicable
@swao/module-iac-scan (SWAO internal)Wrapper that invokes Checkov or KICS and normalises resultsSWAO internal packageBundledNot 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.yaml with header (id: 6, name: twelve_factor), signals and assessment.
  • Portfolio event log entries pass.iac.scanner.start and pass.iac.scanner.complete when a scan runs.

Configuration and limitations ​

Configuration

  • --passes tf runs the pass alone (include inv for language-aware logging checks).
  • Install Checkov or KICS on the PATH to enable TF-IAC findings.

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 config package (config/default.json, config/production.json, config/custom-environment-variables.json) are not read. For Node.js services only process.env reads, the dotenv loader and .env.example count as evidence, and literal hosts in those JSON files are not reported.