Skip to content

checks: in storehound.yaml: project-specific invariants for Info.plist, entitlements, privacy manifest and AndroidManifest #51

Open

Description

@saimskywalker

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 in storehound.yaml with 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 an ignore entry
  • 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, severity and --explain work 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.md documents 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: _Parser allows only fail_on, paths, rules and ignore at the top level (allowOnly(doc, const {...}, 'the file')). Add checks, and parse it before rules and ignore, because _ignore rejects rule ids it does not know (unknown rule ... in ignore).
  • Turn each parsed check into a Rule instance (a ProjectCheckRule in a new lib/src/rules/project_checks.dart) whose why is the check's reason and fix describes the expected value. Then runPreflight in lib/src/engine.dart, the reporters and --explain need no special cases: the CLI passes [...builtInRules, ...config.checkRules].
  • Values: PlistDict in lib/src/plist.dart (string, boolean, array, dict for nested keys, lineOf for line numbers); XmlNode in lib/src/xml_tree.dart (name, attributes, line, descendants(name)).
  • Entitlements per configuration: entitlementsByConfiguration at the bottom of lib/src/rules/ios_config.dart already maps each build configuration to its CODE_SIGN_ENTITLEMENTS file. Move it somewhere shared (lib/src/project.dart) rather than copying it.
  • Tests: test/config_test.dart for parsing errors, a new test/rules/project_checks_test.dart using writeProject and runPreflight from test/support/fixtures.dart, and one case in test/cli_test.dart for --explain check:<name>.

Slices (each one a PR)

  1. Info.plist checks (medium): config parsing and validation, ProjectCheckRule, all operators on info_plist with nested keys, ignore and severity support, config error messages with line numbers.
  2. Privacy manifest and entitlements (small): file: privacy_manifest and file: entitlements with optional configuration:; without configuration, every entitlements file is checked and each finding names its file.
  3. AndroidManifest (small to medium): element plus attribute on 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, add scope: effective so none_of also sees permissions a dependency adds.
  4. 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 as checks: entries.

Out of scope

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

  1. saimskywalker commented on Nov 6, 2026

    @saimskywalker
    OwnerAuthor

    #52 does the first part: checks: for info_plist and android_manifest, all eight operators, required reason, severity, ignore, --explain check:<name>, config errors with line numbers, and a "Project checks" section in doc/configuration.md.

    Still to do:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions