Repository navigation
chore(ci): medium/low findings from the 2026-07-28 full review [platform part] #349
Description
Split from LeanerCloud/cloud-commitments-cli#1594 (cross-component issue, monorepo split)
This issue carries the findings that only apply to files now owned by this repo (terraform/, iac/, arm/, cloudformation/, Dockerfile*, docker-compose*.yml, Makefile.terraform, .env.example, .trivyignore, .snyk, .tflint.hcl, .hadolint.yaml, .dockerignore, and the deploy/migration/rollback workflows), plus this repo's own copy of the Makefile.
Re-verified against the current state of this repo (2026-09-28) before filing: several items from the original 2026-07-28 review have already been fixed since (ci-success treating a skipped job as success, persist-credentials on pre-commit.yml and the deploy workflows, the postgres:16-alpine digest pin in ci.yml and docker-compose.test.yml, and the unquoted-heredoc JSON payloads in deploy-aws-lambda.yml / deploy-aws-fargate.yml / rollback.yml, the last one closed as #1542). Those are dropped from the list below; only items still reproducing against the current tree are kept, with their line numbers re-checked.
The three findings about shared tooling files that are copied identically into all four split repos (pre-commit hook repos pinned to mutable tags, check-yaml excluding workflow files, and the .golangci.yml gocyclo threshold mismatch) are filed as this repo's own item within the same list, since this repo's copy independently reproduces them. cloud-commitments-go has a fourth item (its own Makefile still has the pre-split silent-pass bug that this repo's Makefile does not).
MEDIUM
-
AWS Terraform IaC misconfigurations are ungated end to end
- Where:
.pre-commit-config.yaml:203(entry: bash -c 'trivy config ... --skip-dirs terraform/environments/aws ...');.github/workflows/ci.ymlTrivy IaC scan (scan-type: 'config') still documented inline as using "the default exit-code 0 so misconfig findings are reported [to the Security tab, not gating]". - What: the local and CI pre-commit gate skips the entire AWS environment directory, and CI's Trivy IaC scan reports to the Security tab without failing the job.
- Failure scenario: a CRITICAL misconfiguration introduced under
terraform/environments/aws/(a public S3 bucket,0.0.0.0/0ingress, an unencrypted RDS instance) fails no gate anywhere..trivyignoreadditionally blanket-accepts 15 AVD rule IDs repo-wide rather than per-resource. - Fix direction: drop
--skip-dirs terraform/environments/awsfrom the pre-commit trivy invocation (re-verify the Trivy version installed no longer panics on that directory), and give the CI IaC stepexit-code: 1for CRITICAL now that.trivyignoredocuments the accepted set.
- Where:
-
Makefile security and lint targets pass silently when the tool is absent, and never fail on findings
- Where:
Makefilelint(~:111-116),security-scan-go(~:157-166),security-scan-docker(~:168-176),security-scan-terraform(~:177-184),security-scan-snyk(~:230-239). All still@if command -v <tool> > /dev/null; then <tool> ...; else echo "<tool> not installed"; fi, and eachthenbranch's exit status is the trailing successecho, not the tool's. - What: even when the tool runs and finds issues, the target exits 0; and when the tool is absent it exits 0 too.
- Failure scenario:
make security-scanprints success both on a machine with no gosec installed and on a machine where gosec found ten CRITICAL issues. - Fix direction:
else echo "..."; exit 1; fiin each target, and chain the tool invocation with&&(or drop the trailing successecho) so the tool's exit status propagates.security-scan-terraform(tfsec) should also be retired, since tfsec was removed from CI in favour of Trivy.
- Where:
-
make ciandmake terraform-validateare broken and cannot serve as a local pre-flight- Where:
Makefileterraform-validateloopsfor dir in terraform/environments/*/dev, invoked byci. - What:
terraform/environments/{aws,azure,gcp}are flat directories with.tffiles at the top level (confirmed:terraform/environments/aws/has nodev/subdirectory today). The glob does not expand,cdfails,|| exit 1fails the target. - Failure scenario: the documented local CI pre-flight (
make ci) cannot be used, so nobody runs it, so the never-failingsecurity-scantarget above it is never exercised locally either. - Fix direction: loop
terraform/environments/*and skip non-directories, matching whatci.yml's ownterraform-validatejob already does correctly.
- Where:
-
Snyk is
continue-on-errorand outside theci-successgate- Where:
ci.ymlsnyk-scanjob (continue-on-error: true), not present inci-success'sneeds:list. - What:
ci-success's skipped-job handling was already fixed (it now allowlists onresult == "success"rather than denylistingfailure/cancelled), but Snyk itself is still soft-fail and not a required job, so a CRITICAL dependency finding from Snyk never turns CI red. - Failure scenario: Snyk finds a CRITICAL vulnerability; the job step fails but
continue-on-error: truemarks the job green, andci-successnever looks at it since it isn't inneeds. The PR merges. - Fix direction: add
snyk-scantoci-success'sneeds:and dropcontinue-on-error: true(or make the pass/fail explicit via the job's own outcome rather than swallowing it).
- Where:
-
Trivy filesystem scan and the coverage threshold are advisory only
- Where:
ci.ymlTrivyscan-type: fsstep (comment: "This scan uses the default exit-code 0 ... misconfig findings are reported"); coverage check still emits::warning::Coverage is below 80%and never fails. - What: both appear as gates in the job list and neither is one.
- Failure scenario: a CRITICAL dependency CVE surfaced by Trivy lands in the Security tab and CI stays green; the same PR drops backend coverage 20 points and CI is green on both counts.
- Fix direction: set
exit-code: '1'on the Trivy fs step (.trivyignorealready exists to record accepted findings), and decide whether the 80% coverage line is a gate or documentation; if a gate,exit 1.
- Where:
-
docker-build's smoke test still rebuilds the image and cannot fail- Where:
ci.ymldocker-buildjob:docker/build-push-actionnow builds once withload: trueand GHA caching (an improvement from the reviewed commit), but the "Test Docker image" step still runs a second, uncacheddocker build -t cudly:test .instead of reusing thecudly:${{ github.sha }}tag the previous step already loaded, and both smoke commands still end in|| true. - What: the step passes even if the entrypoint crashes on start, and duplicates the most expensive step in the job.
- Failure scenario: a change to
scripts/entrypoint.shmakes every container exit immediately;docker-buildstill reports success. - Fix direction: reuse the already-loaded
cudly:${{ github.sha }}tag in the smoke-test step instead of rebuilding, and drop|| trueso a non-zero--version/--helpfails the job.
- Where:
-
Get migration versionwrites untrusted command output into$GITHUB_ENV- Where:
.github/workflows/database-migration.yml"Get migration version" step:VERSION=$(migrate ... version 2>&1 || echo "unknown")thenecho "MIGRATION_VERSION=$VERSION" >> "$GITHUB_ENV". - What:
2>&1folds stderr into$VERSION. golang-migrate/pq error text is multi-line and can echo connection details; writing a multi-line value to$GITHUB_ENVinKEY=VALUEform either fails the step with "Invalid format" or, if the text contains a newline followed byNAME=value, injects an arbitrary environment variable into every subsequent step. - Failure scenario: a connection failure produces a multi-line error; the step fails with a confusing "Invalid format" instead of the real DB error, or an error string carrying attacker-influenced text sets an environment variable for later steps. Nothing reads
MIGRATION_VERSIONafterwards, so the step earns nothing in exchange. - Fix direction: capture stdout only, use the heredoc delimiter form for
$GITHUB_ENV, or delete the step since nothing consumes the value.
- Where:
-
No Dependabot or Renovate configuration, and no
CODEOWNERS- Where:
.github/contains onlyrunbooks/andworkflows/, confirmed on the current tree; nodependabot.yml, norenovate.json, noCODEOWNERS. - What: this repo SHA-pins every Action, digest-pins every Docker base image, and version-pins every CI tool. That is good hygiene, and it becomes a liability without update automation.
- Failure scenario: a CVE lands in a pinned base image or Action and nothing notices; without
CODEOWNERS, workflow changes also require no security review. - Fix direction: add
.github/dependabot.ymlcoveringgithub-actions,docker,gomodandnpm(frontend/); addCODEOWNERScovering.github/workflows/**andterraform/**. (This repo's copy of the finding;cloud-commitments-go,-cliand-mcpeach need their own equivalent config since Dependabot/Renovate configs are per-repo.)
- Where:
-
Terraform version still pinned to divergent values across the pipeline
- Where:
ci.ymlandpre-commit.ymlpin1.10.5; the six deploy/cleanup/destroy workflows now share a singleTF_VERSION: '1.10.0'env var each (an improvement over hardcoding it per-step, but still one patch behindci.yml);rollback.ymlpinsTF_VERSION: '1.6.0', four minors behind. - What:
terraform validateruns on 1.10.5 in CI whileterraform applyruns on 1.10.0 (or 1.6.0 for rollback), so a behavioural or provider-resolution difference can pass CI and still break at apply time. - Fix direction: define the version once (a repo-level variable or a
.terraform-versionfile) and reference it from every workflow, includingrollback.yml.
- Where:
LOW
-
.env.exampleships a working admin password and API key- Where:
.env.example:69(ADMIN_PASSWORD_DEV=LocalDev!Pass123) and:71(ADMIN_API_KEY_DEV=cudly-local-dev-api-key-not-for-prod). - What: clearly labelled dev placeholders, but they are functional bootstrap credentials, and the file's own header instructs readers to copy it to
.env.local. - Failure scenario: an operator copies
.env.example, fills in the cloud settings, misses these two lines, and deploys with a publicly known admin password. - Fix direction: replace both values with obviously non-functional placeholders (
<set-me>) so the app fails loudly rather than starting with a known credential.
- Where:
-
deploy-all.ymlfailure check ignores cancelled child deployments- Where:
.github/workflows/deploy-all.ymlstill tests only== "failure"for each of the four child deploy job results; the surrounding comments show this asymmetry withci.yml(which does handlecancelled) is currently a deliberate, but undocumented-as-accepted, gap. - What: a
cancelledreusable-workflow result passes the check and the job still prints "All deployments completed successfully!". - Failure scenario: a production multi-cloud deploy is cancelled mid-
terraform apply, leaving infrastructure half-applied, and the summary reports success. - Fix direction: treat
cancelled(andskipped-when-expected) as failure, matchingci.yml's own handling.
- Where:
-
.dockerignoreomits the test compose/Dockerfile pair it excludes for the dev pair- Where:
.dockerignoreexcludesdocker-compose.ymlandDockerfile.devbut still notdocker-compose.test.ymlorDockerfile.test. - What: both are
COPY . .-ed into the builder stage. No secrets are involved, but it is unnecessary build context and an inconsistency in a file whose entire purpose is this. - Fix direction: add both to the "Docker compose dev files" block.
- Where:
-
Makefile.terraform clean-locksdeletes provider lock files with no confirmation- Where:
Makefile.terraformclean-lockstarget:find terraform/environments -name ".terraform.lock.hcl" -type f -delete. - What:
.terraform.lock.hclis the committed provider checksum pin. Deleting it and re-runninginitre-resolves providers to whatever the~>constraint currently admits. The adjacentcleantarget is correctly scoped and its help text says "preserves .terraform.lock.hcl", so the intent is understood elsewhere in the same file. - Failure scenario: an operator runs
clean-locksto clear a resolution problem, commits the regenerated lock, and the repo's provider supply-chain pinning silently moves to a newer, unreviewed provider build with no diff that reads as a supply-chain change. - Fix direction: require an explicit confirmation variable, and print a warning that the regenerated lock must be reviewed as a supply-chain change.
- Where:
Findings shared with the other three split repos (this repo's own copy still reproduces them)
-
Third-party pre-commit hook repos are pinned to mutable tags, not commit SHAs
- Where:
.pre-commit-config.yaml-rev: v1.105.0(antonbabenko/pre-commit-terraform),rev: v6.0.0(pre-commit/pre-commit-hooks),rev: v0.47.0(igorshubovych/markdownlint-cli). - What: every GitHub Action in this repo is SHA-pinned with a version comment; the pre-commit hook repos, which execute arbitrary code in the same CI job, are pinned only to Git tags, which the upstream owner can move.
- Fix direction: pin each to a full commit SHA with the version kept as a trailing comment, matching the Actions convention.
- Where:
-
check-yamlpre-commit hook excludes every workflow file- Where:
.pre-commit-config.yamlcheck-yamlhook:exclude: '^(docker-compose.*\.yml|\.github/workflows/.*|cloudformation/.*|iac/.*/cloudformation/.*)$'. - What: the only YAML syntax check in the pipeline skips all workflow files, and there is no
actionlintstep anywhere. - Failure scenario: a malformed workflow is committed; GitHub silently refuses to run it (no failing check, no red signal).
- Fix direction: drop
\.github/workflows/.*from the exclusion and add anactionlinthook.
- Where:
-
.golangci.ymlgocyclo threshold (15) differs from the enforcing gates (10)- Where:
.golangci.ymlgocyclo: min-complexity: 15versusci.yml's standalonegocyclo -over 10 -ignore "_test\.go"step and the Makefile'sgocyclo -over 10incomplexity. - What: golangci-lint reports nothing for functions at complexity 11-15; the standalone
gocyclosteps fail them. - Failure scenario: a function lands at complexity 11-15, passes
make lint, and reddensLint Code/pre-commit --all-files, blocking every other PR until fixed (a recorded regression pattern, tracked previously as fix(frontend): stop Enter on a column-filter button from sorting #1388). - Fix direction: set
min-complexity: 10so all tools agree.
- Where:
Findings from the 2026-09-02 codebase audit
Re-verified against the current tree; all five are still current.
A13-013 (medium)
A correction to the .trivyignore AVD-AWS-0013 suppression. Its text claims TLS 1.0 "was tightened in a prior PR" and that the finding only trips because the value is "inherited rather than set explicitly". It is set explicitly: terraform/modules/frontend/aws/main.tf:113 is still minimum_protocol_version = var.acm_certificate_arn != null ? "TLSv1.2_2021" : "TLSv1", and the null branch is the default for every environment. The finding is live rather than an artifact; it is unexploitable only because enable_cdn = false in every shipped tfvars, which is a different reason from the one written down, so the suppression should be rewritten to that reason or the branch dropped. Related to #1627's minimum_protocol_version item, which tracks the code side. Audit finding A13-013.
A14-034 (medium)
The make ci / make terraform-validate item has a larger sibling: scripts/tf-deploy.sh:62 still builds ENV_DIR="${PROJECT_ROOT}/terraform/environments/${PROVIDER}/${PROFILE}", a path this repo does not have (environments are single flat roots selected by -var-file). The script then creates that directory and symlinks only main.tf into it, leaving a root with no variables.tf or backend.tf, and runs terraform init with no -backend-config. Makefile:deploy and every target in Makefile.terraform route through this script, so the documented local deployment path is inoperable in the same way the local CI pre-flight is. Fixing this alongside terraform-validate keeps one directory assumption rather than two. Audit finding A14-034.
A13-016 (low)
Two corrections to the suppression files. First, .trivyignore's GCP-0015 entry says the GCP database module "exposes require_ssl as a variable" and is "Tracked as a hardening follow-up to set require_ssl = true in the module default". No require_ssl variable exists anywhere under terraform/modules/database/gcp/; both ip_configuration blocks hardcode ssl_mode = "ENCRYPTED_ONLY", which already requires TLS. The entry is either stale against a fixed finding or masking a different one. Second, .snyk declares two keys the Snyk policy format does not read: failOnSeverity: high and a license allow/deny map. Both real enforcement points pass the threshold on the command line instead (Makefile and ci.yml both --severity-threshold=high), and nothing reads the license map, so a GPL-3.0 dependency would be admitted despite the file appearing to forbid it. Deleting the two inert blocks, or moving the license policy to a real enforcement point with a comment saying where it lives, removes the false assurance. Audit findings A13-016 and A13-017.
A13-021 (low)
Two more mutable-tag container references besides the postgres:16-alpine ones (which are now digest-pinned in both ci.yml and docker-compose.test.yml). docker-compose.yml's nginx:alpine floats a whole major, and dpage/pgadmin4:9.2 is minor-pinned but not digest-pinned. Scope is the local dev stack only, and the nginx half matters most because scripts/nginx.conf depends on the reverse-proxy behaviour. Worth pinning both in the same pass. Audit finding A13-021.
A14-037 (low)
The .golangci.yml gocyclo item covers the threshold axis; there is a second axis at a third site. Makefile's complexity target still runs gocyclo -over 10 . 2>&1 || true with no -ignore flag, while ci.yml passes -ignore "_test\.go" and .golangci.yml excludes _test\.go. So the local target fails on table-driven tests that the merge gate accepts. Adding -ignore "_test\.go" to the Makefile target belongs in the same change as setting min-complexity: 10. Separately, the 2>&1 || true capture on that line folds a gocyclo tool error into the "complexity issues" branch, so a missing binary reads as findings. Audit finding A14-037. (cloud-commitments-go's Makefile has the identical gap; see the sibling issue there.)
A14-041 (low)
A third gate belongs alongside the Trivy fs scan and the coverage threshold. ci.yml's tfvars format checks still end with terraform fmt -check "github-${env}.tfvars" || echo "Note: github-${env}.tfvars may need formatting", so nothing they find can fail the step, in a step named "Validate environment tfvars files". terraform fmt -check returns non-zero on a genuine parse error as well as on formatting, so a broken github-prod.tfvars reaches the deploy workflows with the validating job green. Simplest fix is to let the non-zero exit propagate, or fold these files into the existing terraform fmt -check -recursive step. Audit finding A14-041.
Acceptance criteria
- Each checklist item above is fixed (or explicitly re-triaged with a documented reason) with evidence that the specific gate now actually fails on the described scenario.
- The three shared-tooling items (mutable-tag pre-commit pins,
check-yamlworkflow exclusion, gocyclo threshold mismatch) are tracked and fixed consistently with the sibling copies of the same finding incloud-commitments-cli,cloud-commitments-go, andcloud-commitments-mcp.
Activity
IAmJSD commentedon Oct 23, 2026
Claiming: working on this
chore(ci): make local gates fail honestly and pin pre-commit hook rep…
chore(ci): scan the AWS root in the trivy hook, pin Terraform in one …