feat: security checks — 14 rules, standalone-capable (v0.9.0)#38
Merged
Conversation
…e config errors - Replace `try?` with `decodeIfPresent` for `layers` and `security` so a present-but-malformed key throws instead of silently becoming nil/[]. - Add `CustomStringConvertible` to `ConfigurationError` covering all four cases with human-readable messages. - Document `isEnabled(ruleID:)` contract: does not check the master `enabled` flag (engine guards separately). - Add two failing-first tests: `testMalformedLayersThrowsAtDecode` and `testMalformedSecuritySectionThrowsAtDecode`.
Also adds .securityIssue to the exhaustive switch in GraphBuilder.reasonLabel.
…n, access control)
… word qualification, case patterns
…'key' Rule now evaluates only the last member component of a.b expressions and bare identifiers — never the base of a member access — so tokens.count, token.kind, key/value loop vars, keyWindow and keyPath are silent. Bare "key" is also removed from the rule-local word list; multiword forms (apiKey, storeKey) still fire via the adjacent-pair join in matchesSensitiveName.
…namespace URLs
DisabledTLSValidationRule: switch from trimmedDescription to token-joined
text so trivia (comments) cannot suppress findings; add cancelAuthenticationChallenge
token guard so TrustKit-style wrapper delegates (which can reject) are never
flagged; SecTrustEvaluate* matched via hasPrefix on token array.
HttpURLLiteralRule: skip bare-empty host (url.hasPrefix("http://") idiom);
add XML-namespace/DTD authority allowlist (www.w3.org, schemas.android.com,
schemas.microsoft.com, schemas.xmlsoap.org, xmlns.com, purl.org, ns.adobe.com,
www.apple.com) and .dtd path suffix skip — opaque identifiers, never fetched.
195 tests, 0 failures.
- Track usedSecurityPreset flag in InitCommand so the standalone path prints "mode: security-only" instead of the misleading "mode: layered, modules: 0" line. - Add blank line in ConfigGenerator.securityPreset() between the two header comment lines and the security: block, matching the appended variant's formatting. - Strengthen testSecuritySectionAppendsToGeneratedConfig: build a real temp fixture project via makeProject, call generate(mode: .layered), append securitySection(), then load and assert security?.enabled == true and layers non-empty.
Self-lint + isowords + Signal-iOS (SignalServiceKit, 1408 files) triage: - highEntropySecret: skip all-distinct-character literals — alphabet/charset tables (e.g. the base64 alphabet in our own rule) are high-entropy by construction but never secrets; real keys repeat characters. - hardcodedSecret: exempt identifier-shaped values — literals echoing their own variable name, and word phrases separated by _ - / or space where each word is letters plus at most 3 trailing digits (storage key names, header names like x-signal-checksum-sha256, REST paths like v2/keys/signed). Random keys keep firing via interior digits or long digit runs. - biometryNoErrorHandling: skip bare statement calls whose result is unused (the documented idiom to populate biometryType makes no auth decision). Signal-iOS production errors: 28 -> 2, both defensible (a real hardcoded Giphy API key; SHA1 usage pending human confirmation). isowords: clean. Each fix carries a regression test reproducing the real-world false positive (211 tests green).
- HttpURLLiteralRule: remove dead "::1" from exemptHosts array (split on ":" mangled both raw http://::1/... and bracket http://[::1]:8080/... forms so neither ever matched). Check for IPv6 loopback before the port-strip step: exempt when host starts with "[::1]" or equals "::1". Two new TDD tests confirm both forms are correctly exempted. - SecurityRuleRegistry: replace three-line TRANSITIONAL doc comment with a one-liner — the registry is now complete (13 Swift rules + cleartextHTTP).
This was referenced Jun 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an opt-in
security:section to.solid.yml:lintnow detects 14 insecure Swift/iOS patterns across Keychain, Crypto, Network, Auth and Logging — alongside (or without) architecture rules.The 14 rules
Provable patterns (default
error):keychainAccessibleAlways,keychainMissingAccessibility,insecureHash(MD5/SHA1),hardcodedSecret,cleartextHTTP(Info.plist ATS off),tokenInUserDefaults,biometryNoErrorHandling,publicPIIInLog,disabledTLSValidation(trust-all handlers).Heuristics (default
warning):printSensitiveData,httpURLLiteral,biometryNoFallback,sensitiveDataInUserDefaults,highEntropySecret.Severity: per-rule
rules.<id>.severity> globalseverity> built-in default;disable: [ruleID]; unknown IDs are a config error.// solid:ignore <reason>suppresses a finding (reason mandatory).Architecture
SecurityRuleprotocol, one struct per rule, one SwiftSyntax parse per file shared by all rules;PlistATSCheckscans Info.plist separately. Findings are regularViolations (new.securityIssuereason +detail), so baseline, text/json/github reporters, exit codes, the GitHub Action and the Claude Code hook work with zero changes.Also in this PR
init --security: standalone security preset (works on empty projects), composable with--tca/--freezelayers:optional + explicit "nothing to check" error on inert configssolid:ignoreparsing factored out and anchored (prose mentioning the directive no longer suppresses)examples/security.solid.yml; CHANGELOG 0.9.0Real-world calibration (done before this PR)
Release notes
Tag
v0.9.0after merge (release pipeline auto-publishes binaries, Homebrew, GitHub Action). The README install pins (v0.8.0) get bumped in the usualchore: releasecommit.