IaC scanning
IaC scanning is its own check, fremforge/iac-scan, with its own settings and its own findings tab. On every pull request, and on every push to the default branch or a protected release branch, fremforge runs trivy config over the repository with all of Trivy’s built-in misconfiguration checks:
- Dockerfiles and Containerfiles
- Docker Compose files
- Kubernetes manifests
- Helm charts
- Terraform and OpenTofu,
.tfand.tofufiles alike, with their remote modules checked too: modules from your organisation on frem.sh and from fremforge’s mirrors, and registry modules and github.com or gitlab.com sources at a tag or a commit through the module cache. Any other remote source is not fetched, and is listed as such - AWS CloudFormation templates, YAML and JSON
- Azure Resource Manager (ARM) templates, JSON
- Bicep files, compiled to ARM first; see Bicep
- Ansible playbooks and tasks, for the cloud resources their modules create (for example
amazon.aws.s3_bucket); not every module is covered
Each of these was proved on 19 October 2026 with a pull request that adds a known misconfiguration (it failed the check) and one with the secure version (it passed).
Until 17 October 2026 these checks ran inside the image scan and their findings were image findings. The image check, fremforge/image-scan, no longer carries them.
The pull request check
fremforge/iac-scan fails when the pull request adds a misconfiguration at or above your threshold. A misconfiguration the branch it merges into already has does not block it: the check compares with the latest scan of that branch, the default branch or a protected release branch. A finding is the same finding when it is the same check in the same file; a second instance of a check in a file is new.
When there is no scan of that branch yet, every finding at or above the threshold blocks, and a scan of the branch is requested, so the next run of the check is compared.
The status says what it found, for example 1 new high/critical misconfiguration(s); merge blocked; 2 already on main, and links to Security › Findings › IaC.
When it is a required check
fremforge/iac-scan posts on every pull request. Whether a failure blocks the merge depends on whether it is a required branch-protection check in your organisation:
- Organisations created on or after 19 October 2026 have it as a required check from their first repository.
- Older organisations get it organisation by organisation, so that no pull request opened before the check existed waits for it. Until then it shows the result without blocking the merge on its own.
- Mode Off removes it from the required checks; Audit keeps it required but never fails.
Security › Policies › Scanners › IaC says which applies to your organisation: A required check in this organisation, or Not yet a required check in this organisation. You can add it to your required checks yourself at any time; see Branch protection.
Settings
Under Security › Policies › Scanners › IaC:
| Setting | Values | Default |
|---|---|---|
| Mode | Enforce (block the merge), Audit (report, never block), Off (do not scan) | Enforce |
| Fail the check at | any severity, medium or above, high or critical, critical only | high or critical |
The same settings are the iac_scan entry of the organisation’s scan policy, so you can set them through the API and per repository:
PUT /_app/api/v1/orgs/{org}/policy
{ "scan_policy": { "iac_scan": { "mode": "enforce", "block_severity": "critical" } } }A per-repository value wins over the organisation’s:
PUT /_app/api/v1/orgs/{org}/repos/{repo}/policy
{ "scan_policy": { "iac_scan": { "mode": "audit" } } }With Terraform or OpenTofu, use fremforge_scan_policy with scanner = "iac_scan" (provider 0.4.0 and later):
resource "fremforge_scan_policy" "iac" {
org = "acme"
scanner = "iac_scan"
mode = "enforce"
block_severity = "high"
}Remote Terraform modules
The check reads the modules your configuration calls, so a misconfiguration inside a module is found too:
Through the module cache. A registry module (
terraform-aws-modules/vpc/aws, with or withoutversion) and a github.com or gitlab.com source at a tag or a commit are read through the dependency proxy’s module cache: the newest version matching yourversionconstraint, pinned on its first fetch. Only the module’s.tfand.tf.jsonfiles, and the local modules they call, are checked; its examples and tests are not. Your repository is never changed: the scanner works on a private copy.From frem.sh. Modules in your own organisation, and in fremforge’s mirrors of public repositories, are read from frem.sh.
From your own git host, with your credential. A module on another git host over https (
git::https://git.example.com/infra/modules.git?ref=v1.2.0, orhttps://git.example.com/infra/modules.git) is read when your organisation holds a read-onlykind: gitcredential for exactly that host. An owner stores it under Security › Access › Registry credentials, choosing Git module host, or through the registry credentials API; the feature is off until an owner turns it on. The scan fetches the module through the dependency proxy, which adds your credential:- read only: a git fetch, never a push;
- your credential goes to that host and nowhere else, and a redirect is refused;
- nothing is cached, so deleting the credential leaves no copy behind.
A host you hold no credential for is not fetched, as before.
Anything else is not fetched. Another host without a credential, a github.com or gitlab.com source without a tag (a branch), an archive URL or an SSH source is never fetched by the scanner. It is listed under Not checked on Security › Findings › IaC, with its source, and the check’s status says how many modules were not fetched; the resources it defines are not checked.
Findings inside a module are listed apart, under In modules, grouped by module with the version the scan read and where it is called from. They never block a pull request, have no fix-by date and send no alert, because the fix is in your call (an input, or a module version that fixes it), not in your files. A dismissal is kept across module versions: it is keyed on the module, the file in it and the check, not the version.
Bicep
Trivy has no Bicep checks, but it checks ARM. So fremforge compiles every .bicep file in the repository to ARM with the Bicep CLI and checks the result with the same checks, threshold and dismissals as the rest of the repository. A finding is shown on the .bicep file, on the line of the flagged property where it can be found, else on the resource declaration, and says that the line was mapped from the compiled ARM.
- Offline. Bicep runs with no network access and without restoring modules. A module from a registry (
br:) or a template spec (ts:) cannot be fetched, so a file that uses one is not compiled and is listed as such. Local modules (module x './y.bicep') work. - Each file on its own. Every
.bicepfile, modules included, is compiled and checked by itself, so a resource in a module is checked in the module’s file. - Your
bicepconfig.jsonis not read. Module aliases, extensions, experimental features and analyzer settings in it have no effect on the scan. A file that needs an experimental feature to compile is listed as not compiled. - Never a silent pass. A file that could not be compiled (an error, a registry module, more than 60 seconds, or past the limit of 200 files or 2 MiB per repository) does not block the merge, because it is not a misconfiguration, but the check’s status says how many were not checked, and Security › Findings › IaC lists them under Files not checked with the reason.
Your own checks
An organisation can add its own checks, written in Rego as Trivy custom checks. fremforge/iac-scan runs them with the built-in checks on every repository of the organisation, under the same mode and threshold, and only what a pull request adds blocks it.
Add them under Security › Policies › IaC checks, or through the API (below). A check must:
- be in a package under
custom., for examplepackage custom.acme.no_root; - carry a
# METADATAblock with atitle, acustom.id(its findings are shown ascustom/<id>) and acustom.severity(LOW,MEDIUM,HIGHorCRITICAL);custom.input.selectornames the file types it reads, as in Trivy; - read only the scanned file (
input): noimport dataand no reference todata; - use no network or runtime built-ins:
http.send,net.lookup_ip_addr,opa.runtime,rego.parse_module,trace,numbers.rangeandnumbers.range_stepare refused.
At most 25 checks per organisation, 32 KiB each. The console offers a worked example to start from.
A saved check is Checking until the scanner has verified it with OPA, in the same fenced environment that runs your pull request scans, and then Active or Rejected, with the reason. Only an active check that is not switched off runs. Editing a check’s source verifies it again; until then the previous verdict no longer applies and it does not run. You can switch a check off without deleting it.
Findings of your checks appear with the built-in ones, in the pull request check and under Security › Findings › IaC, marked Custom with the check’s name.
API
GET /_app/api/v1/orgs/{org}/code-security/iac/policies
POST /_app/api/v1/orgs/{org}/code-security/iac/policies { "name": "No root", "source": "<rego>", "enabled": true }
PUT /_app/api/v1/orgs/{org}/code-security/iac/policies/{id}
DELETE /_app/api/v1/orgs/{org}/code-security/iac/policies/{id}With Terraform or OpenTofu, each check is a fremforge_iac_custom_check (provider 0.4.0 and later): name, source and enabled, with status, status_reason, check_id, title and severity read back from the scanner’s verdict.
resource "fremforge_iac_custom_check" "no_root" {
org = "acme"
name = "No root"
source = file("checks/no_root.rego")
}Listing needs findings:read or policy:read; changes need policy:write. A new or changed check comes back with status pending; the scanner’s verdict sets it to valid or invalid, with status_reason. A check the API can refuse before the scanner sees it (too large, no custom. package, no METADATA) is answered 400 with the reason; the 26th check is answered 409. Every change is in the audit log.
Findings
Security › Findings › IaC lists the open misconfigurations: the check (for example KSV-0014), its title, the repository and the file, with severity and fix-by date. Filter by severity, open the file at the scanned commit, and dismiss one or several with a reason, as on the other findings tabs. A dismissal is kept for the same check in the same file across scans.
For a few checks where one property has a single secure value, such as the S3 public access block and Azure storage’s HTTPS-only and minimum TLS settings, Open fix PR in the Fix column opens a pull request that sets it, without AI. See Rule-based fixes.
They also appear on Security › Findings › All as the type IaC, on the Security overview as their own row, in the compliance report and through the findings API (type=iac).
Benchmarks and NIS2
The compliance report groups the open IaC findings by the benchmark controls their checks test (CIS Docker, CIS Kubernetes, RKE2 and EKS, the NSA Kubernetes hardening guide, Pod Security Standards, CIS AWS), from the compliance mappings Trivy publishes, and by NIS2 Article 21(2) area. The NIS2 grouping is fremforge’s own; it is not an official mapping and does not state compliance with any measure.
To ignore a check in a repository, use a Trivy ignore file; see Trivy ignore files.
Not covered
These checks find misconfigurations. They do not match versions against vulnerability advisories:
- Terraform modules are not checked for vulnerable versions. Terraform and OpenTofu providers are, by the dependency scan; see Terraform and OpenTofu providers.
- Helm charts are not checked for vulnerable chart versions. The container images a chart deploys are, by the image scan; see Images your Helm charts deploy.
- Pulumi and the AWS CDK are not scanned: their resources exist only when your program runs, and fremforge does not run your code.
- Coverage within a format is Trivy’s. For example, network security group rules in ARM templates (and so in Bicep) are not evaluated, and an Ansible task is checked only when Trivy knows its module: in our proof an
amazon.aws.s3_buckettask was checked and anamazon.aws.ec2_security_grouptask was not. - CI action versions are checked, by the dependency scan rather than by this check: an action with a known vulnerability is a dependency finding; see CI action versions. Whether they are pinned to a commit is checked separately; see Workflow pinning.
See Supported ecosystems.