Repository navigation
Description
Why it matters
Teams that care about store safety usually already have a few hand-written tests in their app that parse Info.plist or AndroidManifest.xml and pin invariants nobody else would know about: "UIBackgroundModes is exactly audio and remote-notification, because that is what App Review accepted", "ITSAppUsesNonExemptEncryption is false", "the Release entitlements use aps-environment production", "android:allowBackup is false", "no dependency may add READ_MEDIA_IMAGES". These are real, but they are app-specific, so no built-in rule will ever own them.
So the team ends up with two systems: storehound for the generic checks and a Dart test file for their own pins, each with its own output, its own suppression story, and no SARIF annotations for the second one. #32 plans a guide for moving from hand-written tests to storehound; this issue gives the app-specific pins somewhere to move to.
What done looks like
- A
checks:section instorehound.yamlwith a small, fixed set of assertions on Info.plist, privacy manifest, entitlements and AndroidManifest values - Every check needs a
reason, the same convention as disabling a rule or anignoreentry - Findings appear in text, JSON and SARIF like any rule's, with the file and line of the value (or of the parent element when the value is missing)
-
ignore,severityand--explainwork for a project check - Mistakes in a check (unknown file, unknown operator, wrong value type) are configuration errors with a line number, exit code 2
-
doc/configuration.mddocuments it, with the five common pins from Adoption guide: stability promise, rule-id aliases, new-rule baseline, pinning/fork path and migration from hand-written config tests #32 as examples
Config sketch
checks:
encryption-exempt:
file: info_plist
key: ITSAppUsesNonExemptEncryption
equals: false
reason: "Export compliance is answered in App Store Connect; keeps each upload from asking."
background-modes-pinned:
file: info_plist
key: UIBackgroundModes
equals: [audio, remote-notification]
reason: "The modes App Review accepted. A new one needs review notes first."
ats-no-arbitrary-loads:
file: info_plist
key: NSAppTransportSecurity.NSAllowsArbitraryLoads # dot = nested dict
absent_or_equals: false
reason: "All traffic is HTTPS."
release-push-production:
file: entitlements
configuration: Release # uses CODE_SIGN_ENTITLEMENTS from project.pbxproj
key: aps-environment
equals: production
severity: error
reason: "Release builds must register for production APNs."
no-backup:
file: android_manifest
element: application
attribute: android:allowBackup
equals: "false"
reason: "Tokens live in shared preferences."
no-media-permissions:
file: android_manifest
element: uses-permission
attribute: android:name
none_of: [android.permission.READ_MEDIA_IMAGES, android.permission.READ_MEDIA_VIDEO]
reason: "The app uses the system photo picker only."Operators (keep the list short; each needs a test): exists, absent, equals, absent_or_equals, not_equals, contains (array or element set contains all listed values), none_of, matches (regular expression on a string value). Default severity is error; severity: overrides it.
Findings use the rule id check:<name> (for example check:no-backup). The colon keeps them from ever colliding with a built-in id, and ignore entries can name them.
Where to start
lib/src/config.dart:_Parserallows onlyfail_on,paths,rulesandignoreat the top level (allowOnly(doc, const {...}, 'the file')). Addchecks, and parse it beforerulesandignore, because_ignorerejects rule ids it does not know (unknown rule ... in ignore).- Turn each parsed check into a
Ruleinstance (aProjectCheckRulein a newlib/src/rules/project_checks.dart) whosewhyis the check'sreasonandfixdescribes the expected value. ThenrunPreflightinlib/src/engine.dart, the reporters and--explainneed no special cases: the CLI passes[...builtInRules, ...config.checkRules]. - Values:
PlistDictinlib/src/plist.dart(string,boolean,array,dictfor nested keys,lineOffor line numbers);XmlNodeinlib/src/xml_tree.dart(name,attributes,line,descendants(name)). - Entitlements per configuration:
entitlementsByConfigurationat the bottom oflib/src/rules/ios_config.dartalready maps each build configuration to itsCODE_SIGN_ENTITLEMENTSfile. Move it somewhere shared (lib/src/project.dart) rather than copying it. - Tests:
test/config_test.dartfor parsing errors, a newtest/rules/project_checks_test.dartusingwriteProjectandrunPreflightfromtest/support/fixtures.dart, and one case intest/cli_test.dartfor--explain check:<name>.
Slices (each one a PR)
- Info.plist checks (medium): config parsing and validation,
ProjectCheckRule, all operators oninfo_plistwith nested keys,ignoreandseveritysupport, config error messages with line numbers. - Privacy manifest and entitlements (small):
file: privacy_manifestandfile: entitlementswith optionalconfiguration:; withoutconfiguration, every entitlements file is checked and each finding names its file. - AndroidManifest (small to medium):
elementplusattributeon the app's main manifest. When the plugin manifest reader from Rule android-restricted-permissions: Play declaration-required permissions, read through plugin manifests #10 lands, addscope: effectivesonone_ofalso sees permissions a dependency adds. - Docs (small, good for a first PR once slice 1 is merged): a "Project checks" section in
doc/configuration.md, and the five pins from Adoption guide: stability promise, rule-id aliases, new-rule baseline, pinning/fork path and migration from hand-written config tests #32 written aschecks:entries.
Out of scope
- Scripting, expressions or loading Dart code from the config
- XPath or arbitrary XML queries; element plus attribute covers the pins we have seen
- Gradle files (they are code, not data)
- Per-flavor manifests and plists, until Flavors, app extensions and monorepos: check every Info.plist, entitlements and manifest overlay that ships #15 defines the model
Related
#32 (migration from hand-written config tests; this is the destination for app-specific pins), #10 (built-in, policy-aware forbid for media permissions; checks: is the generic version without policy knowledge), #15 (configurations and flavors), #50 (when a pin turns out to be useful to everyone, it becomes a built-in rule)
Activity
saimskywalker commentedon Nov 6, 2026
#52 does the first part:
checks:forinfo_plistandandroid_manifest, all eight operators, requiredreason,severity,ignore,--explain check:<name>, config errors with line numbers, and a "Project checks" section in doc/configuration.md.Still to do:
file: privacy_manifestfile: entitlementswith optionalconfiguration:(moveentitlementsByConfigurationto project.dart)scope: effectivefor manifest checks once Rule android-restricted-permissions: Play declaration-required permissions, read through plugin manifests #10 landsfeat: project checks for Info.plist and AndroidManifest (#52)
feat: list project checks in --list-rules (#53)