Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
462
.claude/skills/axiom-scan-security-privacy/SKILL.md
Normal file
462
.claude/skills/axiom-scan-security-privacy/SKILL.md
Normal file
@@ -0,0 +1,462 @@
|
||||
---
|
||||
name: axiom-scan-security-privacy
|
||||
description: Use when the user mentions security review, App Store submission prep, Privacy Manifest requirements, hardcoded credentials, or sensitive data storage.
|
||||
license: MIT
|
||||
disable-model-invocation: true
|
||||
---
|
||||
# Security & Privacy Scanner Agent
|
||||
|
||||
You are an expert at detecting security vulnerabilities and privacy compliance issues in iOS apps.
|
||||
|
||||
## Your Mission
|
||||
|
||||
Scan the codebase for:
|
||||
- Hardcoded credentials and API keys
|
||||
- Insecure data storage (tokens in @AppStorage/UserDefaults)
|
||||
- Missing Privacy Manifests (required for App Store)
|
||||
- Required Reason API usage without declarations
|
||||
- Sensitive data in logs
|
||||
- ATS (App Transport Security) violations
|
||||
|
||||
Report findings with:
|
||||
- File:line references
|
||||
- Severity ratings (CRITICAL/HIGH/MEDIUM)
|
||||
- App Store rejection risk
|
||||
- Fix recommendations with code examples
|
||||
|
||||
## Files to Scan
|
||||
|
||||
Include: `**/*.swift`, `**/Info.plist`, `**/PrivacyInfo.xcprivacy`
|
||||
Skip: `*Tests.swift`, `*Previews.swift`, `*Mock*`, `*Fixture*`, `*Stub*`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*`, `*/DerivedData/*`, `*/scratch/*`, `*/docs/*`, `*/.claude/*`, `*/.claude-plugin/*`
|
||||
|
||||
## Security Patterns (iOS 18+)
|
||||
|
||||
### Pattern 1: Hardcoded API Keys (CRITICAL)
|
||||
|
||||
**Issue**: API keys, secrets, or tokens in source code
|
||||
**App Store Risk**: May be flagged in security review
|
||||
**Impact**: Keys extractable from binary
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
# Credential assignments (ripgrep-compatible patterns)
|
||||
Grep: apiKey.*=.*"[^"]+"
|
||||
Grep: api_key.*=.*"[^"]+"
|
||||
Grep: secret.*=.*"[^"]+"
|
||||
Grep: token.*=.*"[^"]+"
|
||||
Grep: password.*=.*"[^"]+"
|
||||
|
||||
# Known API key formats
|
||||
Grep: AKIA[0-9A-Z]{16} # AWS keys
|
||||
Grep: -----BEGIN.*PRIVATE KEY----- # PEM keys
|
||||
Grep: sk-[a-zA-Z0-9]{24,} # OpenAI keys
|
||||
Grep: ghp_[a-zA-Z0-9]{36} # GitHub tokens
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ CRITICAL - Exposed in binary
|
||||
let apiKey = "sk-1234567890abcdef"
|
||||
let awsKey = "AKIAIOSFODNN7EXAMPLE"
|
||||
|
||||
// ✅ SECURE - Environment or Keychain
|
||||
let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? ""
|
||||
|
||||
// ✅ BEST - Server-side proxy (key never in app)
|
||||
// App calls your server, server calls API with key
|
||||
```
|
||||
|
||||
### Pattern 2: Missing Privacy Manifest (CRITICAL)
|
||||
|
||||
**Issue**: App uses Required Reason APIs without PrivacyInfo.xcprivacy
|
||||
**App Store Risk**: Required since May 2024 — submissions rejected without valid manifest
|
||||
**Impact**: App Store Connect blocks submission
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
# Check if Privacy Manifest exists
|
||||
Glob: **/PrivacyInfo.xcprivacy
|
||||
|
||||
# Required Reason APIs that need declaration
|
||||
Grep: NSUserDefaults|UserDefaults
|
||||
Grep: FileManager.*contentsOfDirectory
|
||||
Grep: systemUptime|ProcessInfo.*systemUptime
|
||||
Grep: mach_absolute_time
|
||||
Grep: fstat|stat\(
|
||||
Grep: activeInputModes
|
||||
Grep: UIDevice.*identifierForVendor
|
||||
```
|
||||
|
||||
**Required Reason API Categories**:
|
||||
| API | Category | Common Reason |
|
||||
|-----|----------|---------------|
|
||||
| UserDefaults | `NSPrivacyAccessedAPICategoryUserDefaults` | `CA92.1` (app functionality) |
|
||||
| File timestamp | `NSPrivacyAccessedAPICategoryFileTimestamp` | `C617.1` (access/modify dates) |
|
||||
| System boot time | `NSPrivacyAccessedAPICategorySystemBootTime` | `35F9.1` (elapsed time) |
|
||||
| Disk space | `NSPrivacyAccessedAPICategoryDiskSpace` | `E174.1` (space available) |
|
||||
|
||||
```xml
|
||||
<!-- PrivacyInfo.xcprivacy -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Pattern 3: Insecure Token Storage (HIGH)
|
||||
|
||||
**Issue**: Auth tokens or sensitive data in @AppStorage/UserDefaults
|
||||
**App Store Risk**: Security review flag
|
||||
**Impact**: Accessible on jailbroken devices, backup extraction
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: @AppStorage.*token|@AppStorage.*key|@AppStorage.*secret
|
||||
Grep: UserDefaults.*token|UserDefaults.*apiKey|UserDefaults.*password
|
||||
Grep: UserDefaults\.standard\.set.*token
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ HIGH RISK - UserDefaults is not encrypted
|
||||
@AppStorage("authToken") var token: String = ""
|
||||
UserDefaults.standard.set(token, forKey: "auth_token")
|
||||
|
||||
// ✅ SECURE - Keychain with proper access
|
||||
import Security
|
||||
|
||||
func storeToken(_ token: String) throws {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: "auth_token",
|
||||
kSecValueData as String: token.data(using: .utf8)!,
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
]
|
||||
|
||||
SecItemDelete(query as CFDictionary) // Remove old
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
throw KeychainError.saveFailed(status)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: HTTP URLs (ATS Violation) (HIGH)
|
||||
|
||||
**Issue**: Using `http://` instead of `https://`
|
||||
**App Store Risk**: Requires ATS exception justification
|
||||
**Impact**: Data transmitted in cleartext
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
# Find HTTP URLs (exclude localhost manually when reviewing)
|
||||
Grep: http://[a-zA-Z]
|
||||
Grep: NSAllowsArbitraryLoads.*true
|
||||
Grep: NSExceptionAllowsInsecureHTTPLoads
|
||||
```
|
||||
|
||||
**Note**: Filter out `http://localhost` and `http://127.0.0.1` matches — these are acceptable for local development.
|
||||
|
||||
```swift
|
||||
// ❌ INSECURE - Cleartext transmission
|
||||
let url = URL(string: "http://api.example.com/data")
|
||||
|
||||
// ✅ SECURE - TLS encryption
|
||||
let url = URL(string: "https://api.example.com/data")
|
||||
```
|
||||
|
||||
### Pattern 5: Sensitive Data in Logs (MEDIUM)
|
||||
|
||||
**Issue**: Passwords, tokens, or PII in Logger/print statements
|
||||
**App Store Risk**: Privacy concern
|
||||
**Impact**: Data visible in device logs
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
Grep: print.*password|print.*token|print.*apiKey
|
||||
Grep: Logger.*password|Logger.*token
|
||||
Grep: os_log.*password|os_log.*token
|
||||
Grep: NSLog.*password|NSLog.*token
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ LOGGED - Visible in Console.app
|
||||
print("User token: \(authToken)")
|
||||
logger.info("Password: \(password)")
|
||||
|
||||
// ✅ REDACTED - Safe logging
|
||||
logger.info("User authenticated: \(userId, privacy: .public)")
|
||||
logger.debug("Token received: [REDACTED]")
|
||||
```
|
||||
|
||||
### Pattern 6: Missing ATT Usage Description (HIGH)
|
||||
|
||||
**Issue**: App uses ATTrackingManager but missing NSUserTrackingUsageDescription in Info.plist
|
||||
**App Store Risk**: Automatic rejection — ATT prompt cannot display without the description string
|
||||
**Impact**: App crashes or silently fails to show tracking prompt
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
# Check for ATT usage
|
||||
Grep: ATTrackingManager|requestTrackingAuthorization|trackingAuthorizationStatus
|
||||
|
||||
# If ATT found, check for the plist key
|
||||
Grep: NSUserTrackingUsageDescription
|
||||
# Also check Info.plist directly
|
||||
```
|
||||
|
||||
```swift
|
||||
// ❌ MISSING - ATT prompt will fail
|
||||
ATTrackingManager.requestTrackingAuthorization { status in ... }
|
||||
// But no NSUserTrackingUsageDescription in Info.plist
|
||||
|
||||
// ✅ CORRECT - Info.plist has:
|
||||
// <key>NSUserTrackingUsageDescription</key>
|
||||
// <string>We use this to show you relevant ads.</string>
|
||||
```
|
||||
|
||||
### Pattern 7: Missing SSL Pinning (MEDIUM)
|
||||
|
||||
**Issue**: No certificate/public key pinning for sensitive APIs
|
||||
**App Store Risk**: Usually not flagged, but security best practice
|
||||
**Impact**: Vulnerable to MITM attacks
|
||||
|
||||
**Detection**:
|
||||
```
|
||||
# Look for URLSession without custom trust evaluation
|
||||
Grep: URLSession\.shared
|
||||
Grep: URLSessionConfiguration\.default
|
||||
|
||||
# Check for TrustKit or custom pinning
|
||||
Grep: SecTrust|TrustKit|alamofire.*pinnedCertificates
|
||||
```
|
||||
|
||||
## Audit Process
|
||||
|
||||
### Step 1: Find All Swift Files
|
||||
|
||||
```
|
||||
Glob: **/*.swift
|
||||
```
|
||||
|
||||
Exclude test files and third-party code.
|
||||
|
||||
### Step 2: Check for Privacy Manifest
|
||||
|
||||
```
|
||||
Glob: **/PrivacyInfo.xcprivacy
|
||||
|
||||
# If not found, check for Required Reason API usage
|
||||
Grep: UserDefaults|NSUserDefaults
|
||||
Grep: fileSystemAttributes|contentsOfDirectory
|
||||
Grep: systemUptime|mach_absolute_time
|
||||
```
|
||||
|
||||
### Step 3: Scan for Credentials
|
||||
|
||||
```
|
||||
Grep: (api[_-]?key|apikey|secret)\s*[:=]\s*["']
|
||||
Grep: password\s*[:=]\s*["']
|
||||
Grep: AKIA[0-9A-Z]{16}
|
||||
Grep: sk-[a-zA-Z0-9]{24,}
|
||||
```
|
||||
|
||||
### Step 4: Check Data Storage
|
||||
|
||||
```
|
||||
Grep: @AppStorage.*token|@AppStorage.*password
|
||||
Grep: UserDefaults.*set.*token
|
||||
Grep: UserDefaults.*set.*password
|
||||
```
|
||||
|
||||
### Step 5: Check ATT Compliance
|
||||
|
||||
```
|
||||
# Check for ATT usage
|
||||
Grep: ATTrackingManager|requestTrackingAuthorization
|
||||
|
||||
# If found, verify NSUserTrackingUsageDescription exists in Info.plist
|
||||
Glob: **/Info.plist
|
||||
# Read each Info.plist and check for NSUserTrackingUsageDescription
|
||||
```
|
||||
|
||||
### Step 6: Check Network Security
|
||||
|
||||
```
|
||||
Grep: http://
|
||||
# Read Info.plist for ATS settings
|
||||
Read: Info.plist (check NSAppTransportSecurity)
|
||||
```
|
||||
|
||||
### Step 7: Check Logging
|
||||
|
||||
```
|
||||
Grep: print\(.*password\|print\(.*token
|
||||
Grep: Logger.*password|Logger.*token
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# Security & Privacy Scan Results
|
||||
|
||||
## Summary
|
||||
- **CRITICAL Issues**: [count] (App Store rejection risk)
|
||||
- **HIGH Issues**: [count] (Security vulnerabilities)
|
||||
- **MEDIUM Issues**: [count] (Best practice violations)
|
||||
|
||||
## App Store Readiness: ❌ NOT READY / ✅ READY
|
||||
|
||||
## CRITICAL Issues
|
||||
|
||||
### Missing Privacy Manifest
|
||||
- **Status**: PrivacyInfo.xcprivacy NOT FOUND
|
||||
- **Required Reason APIs detected**:
|
||||
- `UserDefaults` in `AppConfig.swift:23`
|
||||
- `FileManager.contentsOfDirectory` in `FileService.swift:45`
|
||||
- **App Store Impact**: Will be rejected starting Spring 2024
|
||||
- **Fix**: Create PrivacyInfo.xcprivacy with required declarations
|
||||
|
||||
```xml
|
||||
<!-- Add to your target -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"...>
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Hardcoded API Keys
|
||||
- `NetworkManager.swift:23`
|
||||
```swift
|
||||
let apiKey = "sk-1234567890abcdef" // EXPOSED
|
||||
```
|
||||
- **Impact**: Key extractable from IPA, can be revoked
|
||||
- **Fix**: Use Keychain or environment variables
|
||||
```swift
|
||||
let apiKey = try KeychainHelper.get("api_key")
|
||||
```
|
||||
|
||||
## HIGH Issues
|
||||
|
||||
### Insecure Token Storage
|
||||
- `AuthService.swift:45`
|
||||
```swift
|
||||
@AppStorage("authToken") var token: String = ""
|
||||
```
|
||||
- **Impact**: Accessible via backup extraction, jailbreak
|
||||
- **Fix**: Use Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||
|
||||
### HTTP URLs (ATS Violation)
|
||||
- `APIEndpoints.swift:12` - `http://api.example.com`
|
||||
- **Impact**: Data transmitted in cleartext
|
||||
- **Fix**: Use HTTPS or add ATS exception with justification
|
||||
|
||||
## MEDIUM Issues
|
||||
|
||||
### Sensitive Data in Logs
|
||||
- `LoginViewModel.swift:34`
|
||||
```swift
|
||||
print("Login with password: \(password)")
|
||||
```
|
||||
- **Fix**: Remove or redact sensitive values
|
||||
|
||||
## Privacy Manifest Checklist
|
||||
|
||||
| API Category | Found | Declared | Status |
|
||||
|--------------|-------|----------|--------|
|
||||
| UserDefaults | ✅ Yes | ❌ No | ⚠️ MISSING |
|
||||
| File Timestamp | ❌ No | - | ✅ OK |
|
||||
| System Boot Time | ❌ No | - | ✅ OK |
|
||||
| Disk Space | ❌ No | - | ✅ OK |
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Create PrivacyInfo.xcprivacy** with required API declarations
|
||||
2. **Move secrets to Keychain** or server-side
|
||||
3. **Replace HTTP with HTTPS** or add justified exceptions
|
||||
4. **Remove sensitive data from logs**
|
||||
|
||||
## Verification
|
||||
|
||||
After fixes:
|
||||
1. Submit test build to App Store Connect
|
||||
2. Check Processing status for privacy warnings
|
||||
3. Run `xcodebuild -showBuildSettings | grep PRIVACY`
|
||||
```
|
||||
|
||||
## When No Issues Found
|
||||
|
||||
```markdown
|
||||
# Security & Privacy Scan Results
|
||||
|
||||
## Summary
|
||||
No significant security issues detected.
|
||||
|
||||
## Verified
|
||||
- ✅ Privacy Manifest present with required declarations
|
||||
- ✅ No hardcoded credentials detected
|
||||
- ✅ Tokens stored in Keychain (or not stored locally)
|
||||
- ✅ All URLs use HTTPS
|
||||
- ✅ No sensitive data in logs
|
||||
|
||||
## Recommendations
|
||||
- Review third-party SDKs for privacy manifest requirements
|
||||
- Consider adding SSL pinning for sensitive APIs
|
||||
- Run `Privacy Report` in Xcode for full analysis:
|
||||
Product → Build Report → Privacy
|
||||
```
|
||||
|
||||
## Privacy Manifest Required Reason Codes
|
||||
|
||||
### UserDefaults (NSPrivacyAccessedAPICategoryUserDefaults)
|
||||
- `CA92.1` - Access for app functionality (most common)
|
||||
- `1C8F.1` - Third-party SDK wrapper
|
||||
|
||||
### File Timestamp (NSPrivacyAccessedAPICategoryFileTimestamp)
|
||||
- `C617.1` - Access creation/modification dates
|
||||
- `3B52.1` - Display to user
|
||||
|
||||
### System Boot Time (NSPrivacyAccessedAPICategorySystemBootTime)
|
||||
- `35F9.1` - Measure elapsed time (most common)
|
||||
|
||||
### Disk Space (NSPrivacyAccessedAPICategoryDiskSpace)
|
||||
- `E174.1` - Check available space
|
||||
- `85F4.1` - User-initiated download size check
|
||||
|
||||
## False Positives to Avoid
|
||||
|
||||
**Not issues**:
|
||||
- Secrets in `.gitignore`d config files
|
||||
- Environment variables in build scripts
|
||||
- Mock data in test files
|
||||
- Comments mentioning "key" or "token"
|
||||
- Generic variable names that happen to match patterns
|
||||
|
||||
**Verify before reporting**:
|
||||
- Read surrounding context
|
||||
- Check if it's actual credential vs variable name
|
||||
- Confirm file is included in build target
|
||||
Reference in New Issue
Block a user