build-time-secret-injection — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited build-time-secret-injection (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Any task that introduces or wires values which are:
Info.plist, visible in shipped binary, observable in network traffic), ANDExamples:
.p8 key, key-id, issuer ID, ASC numeric app-idDo NOT invoke for:
Layer 1 — Build-time secrets (consumed by Xcode build process)
Tuist/
├── <Domain>.xcconfig # gitignored, real values
├── <Domain>.xcconfig.example # committed, sandbox values + structure
├── Signing.xcconfig # existing precedent (gitignored)
└── Signing.xcconfig.example # existing precedent (committed)KEY = VALUE pairsProject.swift declares per-target settings(configurations: [.debug(name:, xcconfig:), .release(name:, xcconfig:)]) pointing at the file$(KEY) substitution to embed values at compile timeBundle.main.object(forInfoDictionaryKey: "...") — guarded against nil / empty / unresolved $() tokenci_scripts/ci_post_clone.sh): reads XCC env vars (stored as Secrets in ASC → Xcode Cloud → Workflow → Environment Variables) and generates the xcconfig file before tuist generate runsLayer 2 — CLI tooling secrets (consumed by `swift run <CLI>` etc.)
secrets/
├── .env # gitignored, real values
├── .env.example # committed, structure + docstring
├── <Domain>AuthKey_*.p8 # gitignored binary cert
└── .gitignore # deny-by-default: */!*.example/!README.md.env is KEY=VALUE shell-stylesource secrets/.env && swift run <CLI> --flag-using-$KEY ...let sudokuTarget = Target.target(
// ...
settings: .settings(
base: ["SWIFT_VERSION": "6"],
configurations: [
.debug(name: "Debug", xcconfig: "Tuist/Config-Debug.xcconfig"),
.release(name: "Release", xcconfig: "Tuist/Config-Release.xcconfig"),
]
)
)Wrap multiple xcconfigs via a Config-{Debug,Release}.xcconfig that #include? both Signing + AdMob (Tuist's xcconfig: arg takes a single path).
ci_post_clone.shWhen one repo ships multiple app schemes (e.g. AppA + AppB), XCC sets $CI_PRODUCT and $CI_XCODE_SCHEME per workflow. Case-switch on the scheme to pick the right env-var prefix:
case "${CI_XCODE_SCHEME:-${CI_PRODUCT:-}}" in
AppA)
APP_ID="${APPA_ADMOB_APP_ID:?missing APPA_ADMOB_APP_ID}"
BANNER_UNIT_ID="${APPA_ADMOB_BANNER_UNIT_ID:?missing APPA_ADMOB_BANNER_UNIT_ID}"
;;
AppB)
APP_ID="${APPB_ADMOB_APP_ID:?missing APPB_ADMOB_APP_ID}"
BANNER_UNIT_ID="${APPB_ADMOB_BANNER_UNIT_ID:?missing APPB_ADMOB_BANNER_UNIT_ID}"
;;
*)
echo "Unknown CI_XCODE_SCHEME: ${CI_XCODE_SCHEME:-}" >&2
exit 1
;;
esac
cat > Tuist/AdMob.xcconfig <<EOF
ADMOB_APP_ID = ${APP_ID}
ADMOB_BANNER_UNIT_ID = ${BANNER_UNIT_ID}
EOFRun before tuist generate so the per-target xcconfig reference resolves.
If the project uses a hand-edited .xcodeproj, the equivalent storage is Config/*.xcconfig referenced via target → Build Settings → Base Configuration. Pattern is otherwise unchanged. Tuist regen / clobbering concerns don't apply; manual sync remains your responsibility.
The substitution-resolution check must run against the built bundle's Info.plist, not the source-tree Info.plist:
// ❌ WRONG — reads source plist, gets literal "$(GADBannerUnitID)" — passes falsely
let plist = try PropertyListSerialization.propertyList(from: sourceData, ...)
#expect((plist["GADBannerUnitID"] as? String)?.isEmpty == false) // passes for "$(...)" string
// ✅ RIGHT — combine source-plist key-presence test + runtime guard in code
// Source test catches "someone deleted the key"; runtime guard catches "substitution failed"
guard
let bannerID = Bundle.main.object(forInfoDictionaryKey: "GADBannerUnitID") as? String,
!bannerID.isEmpty,
!bannerID.hasPrefix("$(")
else { preconditionFailure("...") }A future PR should add a build-phase script that asserts no $() literals survived substitution into the built .app/Info.plist. Until then, the runtime guard is the catch.
fatalError("REPLACE_IN_v2.5.3:...") pattern is acceptable as a TRANSITIONAL guard paired with xcconfig migration (see Migration §3), but is forbidden as a long-term standalone solution. Once xcconfig is in place, replace with: Info.plist $() + runtime guard verifying Bundle.main.object(forInfoDictionaryKey:) returns non-empty AND non-$(...).source admob.env && xcodebuild archive does NOT populate $(SUDOKU_ADMOB_APP_ID). Only positional xcodebuild VAR=value or -xcconfig override.xcconfig actually injects, OR a CI script writes the xcconfig file before build. Architect review §A documents this fatal assumption.as? String + guard let ... else { preconditionFailure } with the unresolved-$() check.secrets/.gitignore deny-list (* / !*.example / !README.md) PLUS root .gitignore rules Tuist/*.xcconfig + !Tuist/*.xcconfig.example so neither slips through default-add operations.Tuist/<Domain>.xcconfig exists but is NOT referenced in Project.swift's .settings(configurations:), Tuist regen drops it from the project. Verify Project.swift wiring before assuming xcconfig is active.swift run / CLI scripts / shell → Layer 2 .env.example file with sandbox/test default value.example describing purpose + where to find the real one (cite project memory file by name, NEVER the literal value)$(KEY) substitution to Info.plist; add reading code via Bundle.main with guard (cover nil / empty / $(...) literal); add smoke test for key presence in source plistci_post_clone.sh to write the new KEY from XCC env var with ${VAR:?missing message} fail-fast; if multi-app, branch on $CI_XCODE_SCHEMEgrep -r "<real-prod-value>" . (excluding gitignored dirs) — must return zero hits<domain>-credentials.md to record real values + reference this skill by name.gitignore has Tuist/*.xcconfig + !Tuist/*.xcconfig.examplesecrets/.gitignore inner deny-list present (* / !*.example / !README.md)Project.swift per-target .settings(configurations:) references the xcconfigInfo.plist uses $(KEY) substitution for each secretBundle.main.object(forInfoDictionaryKey:) with guard (NOT as!)nil, empty, and $(...) literalci_post_clone.sh writes xcconfig BEFORE tuist generatecase on $CI_XCODE_SCHEME selects per-app env varsgrep -r "<real-prod-value>" . returns zero hits across all tracked filessecrets/.env carries per-app production pairs (e.g. APPA_ADMOB_APP_ID / APPA_ADMOB_BANNER_UNIT_ID and APPB_* twins). Two consumers render Tuist/AdMob.xcconfig from them: XCC ci_post_clone.sh (from workflow Secret env vars) and your TestFlight upload task (from secrets/.env). Values live in secrets/.env (primary) + the XCC workflow config + the project-memory files as recovery backup — never in code, comments, or diffs.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.