ios-app-review-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +42 -0
- package/.github/actions/ios-review/action.yml +106 -0
- package/.github/workflows/ci.yml +103 -0
- package/.github/workflows/publish.yml +57 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +175 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bitrise/step.sh +128 -0
- package/bitrise/step.yml +101 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzers/asc-iap.d.ts.map +1 -0
- package/dist/analyzers/asc-metadata.d.ts.map +1 -0
- package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
- package/dist/analyzers/asc-version.d.ts.map +1 -0
- package/dist/analyzers/code-scanner.d.ts.map +1 -0
- package/dist/analyzers/deprecated-api.d.ts.map +1 -0
- package/dist/analyzers/entitlements.d.ts.map +1 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/info-plist.d.ts.map +1 -0
- package/dist/analyzers/privacy.d.ts.map +1 -0
- package/dist/analyzers/private-api.d.ts.map +1 -0
- package/dist/analyzers/security.d.ts.map +1 -0
- package/dist/analyzers/ui-ux.d.ts.map +1 -0
- package/dist/asc/auth.d.ts.map +1 -0
- package/dist/asc/client.d.ts.map +1 -0
- package/dist/asc/endpoints/apps.d.ts.map +1 -0
- package/dist/asc/endpoints/iap.d.ts.map +1 -0
- package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
- package/dist/asc/endpoints/versions.d.ts.map +1 -0
- package/dist/asc/errors.d.ts.map +1 -0
- package/dist/asc/index.d.ts.map +1 -0
- package/dist/asc/types.d.ts.map +1 -0
- package/dist/badge/generator.d.ts.map +1 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/types.d.ts.map +1 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/guidelines/database.d.ts.map +1 -0
- package/dist/guidelines/index.d.ts.map +1 -0
- package/dist/guidelines/matcher.d.ts.map +1 -0
- package/dist/guidelines/types.d.ts.map +1 -0
- package/dist/history/comparator.d.ts.map +1 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +994 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/plist.d.ts.map +1 -0
- package/dist/parsers/xcodeproj.d.ts.map +1 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/reporter.d.ts.map +1 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/index.d.ts.map +1 -0
- package/dist/reports/json.d.ts.map +1 -0
- package/dist/reports/markdown.d.ts.map +1 -0
- package/dist/reports/types.d.ts.map +1 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/loader.d.ts.map +1 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/docs/ANALYZERS.md +237 -0
- package/docs/API.md +308 -0
- package/docs/BADGES.md +130 -0
- package/docs/CI_CD.md +283 -0
- package/docs/CLI.md +140 -0
- package/docs/REPORTS.md +212 -0
- package/docs/ROADMAP.md +267 -0
- package/docs/RULES.md +182 -0
- package/docs/SECURITY.md +89 -0
- package/docs/TROUBLESHOOTING.md +227 -0
- package/docs/tutorials/ASC_SETUP.md +188 -0
- package/docs/tutorials/CI_INTEGRATION.md +292 -0
- package/docs/tutorials/CUSTOM_RULES.md +291 -0
- package/docs/tutorials/GETTING_STARTED.md +226 -0
- package/docs/video-scripts/01-introduction.md +106 -0
- package/docs/video-scripts/02-cli-usage.md +120 -0
- package/docs/video-scripts/03-ci-integration.md +198 -0
- package/eslint.config.js +33 -0
- package/examples/.ios-review-rules.json +82 -0
- package/examples/bitrise-workflow.yml +129 -0
- package/examples/fastlane-lane.rb +71 -0
- package/examples/github-action.yml +147 -0
- package/fastlane/Fastfile.example +114 -0
- package/fastlane/README.md +99 -0
- package/jest.config.js +36 -0
- package/package.json +65 -0
- package/scripts/benchmark.ts +112 -0
- package/scripts/debug-parser.ts +37 -0
- package/scripts/debug-pbxproj.ts +36 -0
- package/scripts/debug-specific.ts +47 -0
- package/scripts/test-analyze.ts +67 -0
- package/scripts/xcode-cloud-review.sh +167 -0
- package/src/analyzer.ts +227 -0
- package/src/analyzers/asc-iap.ts +300 -0
- package/src/analyzers/asc-metadata.ts +326 -0
- package/src/analyzers/asc-screenshots.ts +310 -0
- package/src/analyzers/asc-version.ts +368 -0
- package/src/analyzers/code-scanner.ts +408 -0
- package/src/analyzers/deprecated-api.ts +390 -0
- package/src/analyzers/entitlements.ts +345 -0
- package/src/analyzers/index.ts +12 -0
- package/src/analyzers/info-plist.ts +409 -0
- package/src/analyzers/privacy.ts +376 -0
- package/src/analyzers/private-api.ts +377 -0
- package/src/analyzers/security.ts +327 -0
- package/src/analyzers/ui-ux.ts +509 -0
- package/src/asc/auth.ts +204 -0
- package/src/asc/client.ts +258 -0
- package/src/asc/endpoints/apps.ts +115 -0
- package/src/asc/endpoints/iap.ts +171 -0
- package/src/asc/endpoints/screenshots.ts +164 -0
- package/src/asc/endpoints/versions.ts +174 -0
- package/src/asc/errors.ts +109 -0
- package/src/asc/index.ts +108 -0
- package/src/asc/types.ts +369 -0
- package/src/badge/generator.ts +48 -0
- package/src/badge/index.ts +2 -0
- package/src/badge/types.ts +5 -0
- package/src/cache/file-cache.ts +75 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/types.ts +10 -0
- package/src/cli/commands/help.ts +41 -0
- package/src/cli/commands/scan.ts +44 -0
- package/src/cli/commands/version.ts +12 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/types.ts +17 -0
- package/src/git/diff.ts +21 -0
- package/src/git/index.ts +2 -0
- package/src/git/types.ts +5 -0
- package/src/guidelines/database.ts +344 -0
- package/src/guidelines/index.ts +4 -0
- package/src/guidelines/matcher.ts +84 -0
- package/src/guidelines/types.ts +28 -0
- package/src/history/comparator.ts +114 -0
- package/src/history/index.ts +3 -0
- package/src/history/store.ts +135 -0
- package/src/history/types.ts +40 -0
- package/src/index.ts +1113 -0
- package/src/parsers/index.ts +3 -0
- package/src/parsers/plist.ts +253 -0
- package/src/parsers/xcodeproj.ts +265 -0
- package/src/progress/index.ts +2 -0
- package/src/progress/reporter.ts +65 -0
- package/src/progress/types.ts +9 -0
- package/src/reports/html.ts +322 -0
- package/src/reports/index.ts +20 -0
- package/src/reports/json.ts +92 -0
- package/src/reports/markdown.ts +187 -0
- package/src/reports/types.ts +26 -0
- package/src/rules/engine.ts +121 -0
- package/src/rules/index.ts +3 -0
- package/src/rules/loader.ts +83 -0
- package/src/rules/types.ts +25 -0
- package/src/types/index.ts +247 -0
- package/tests/analyzer.test.ts +142 -0
- package/tests/analyzers/asc-iap.test.ts +228 -0
- package/tests/analyzers/asc-metadata.test.ts +210 -0
- package/tests/analyzers/asc-screenshots.test.ts +135 -0
- package/tests/analyzers/asc-version.test.ts +259 -0
- package/tests/analyzers/code-scanner.test.ts +745 -0
- package/tests/analyzers/deprecated-api.test.ts +286 -0
- package/tests/analyzers/entitlements.test.ts +411 -0
- package/tests/analyzers/info-plist.test.ts +148 -0
- package/tests/analyzers/privacy.test.ts +623 -0
- package/tests/analyzers/private-api.test.ts +255 -0
- package/tests/analyzers/security.test.ts +300 -0
- package/tests/analyzers/ui-ux.test.ts +357 -0
- package/tests/asc/auth.test.ts +189 -0
- package/tests/asc/client.test.ts +207 -0
- package/tests/asc/endpoints.test.ts +1359 -0
- package/tests/badge/generator.test.ts +73 -0
- package/tests/cache/file-cache.test.ts +124 -0
- package/tests/cli/cli-index.test.ts +510 -0
- package/tests/cli/commands.test.ts +67 -0
- package/tests/cli/scan.test.ts +152 -0
- package/tests/git/diff.test.ts +69 -0
- package/tests/guidelines/matcher.test.ts +209 -0
- package/tests/history/comparator.test.ts +272 -0
- package/tests/history/store.test.ts +200 -0
- package/tests/integration/cli.test.ts +95 -0
- package/tests/integration/e2e.test.ts +130 -0
- package/tests/parsers/plist.test.ts +240 -0
- package/tests/parsers/xcodeproj.test.ts +289 -0
- package/tests/progress/reporter.test.ts +117 -0
- package/tests/reports/html.test.ts +176 -0
- package/tests/reports/json.test.ts +235 -0
- package/tests/reports/markdown.test.ts +196 -0
- package/tests/rules/engine.test.ts +229 -0
- package/tests/rules/loader.test.ts +187 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.json +27 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import type {
|
|
5
|
+
Analyzer,
|
|
6
|
+
AnalysisResult,
|
|
7
|
+
AnalyzerOptions,
|
|
8
|
+
Issue,
|
|
9
|
+
XcodeProject,
|
|
10
|
+
} from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Patterns to scan for in source code
|
|
14
|
+
*/
|
|
15
|
+
interface ScanPattern {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
severity: 'error' | 'warning' | 'info';
|
|
20
|
+
pattern: RegExp;
|
|
21
|
+
guideline?: string;
|
|
22
|
+
suggestion?: string;
|
|
23
|
+
fileTypes?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SCAN_PATTERNS: ScanPattern[] = [
|
|
27
|
+
// IPv4 hardcoded addresses (IPv6 compliance)
|
|
28
|
+
{
|
|
29
|
+
id: 'hardcoded-ipv4',
|
|
30
|
+
title: 'Hardcoded IPv4 address',
|
|
31
|
+
description: 'Hardcoded IPv4 addresses may cause issues on IPv6-only networks.',
|
|
32
|
+
severity: 'warning',
|
|
33
|
+
pattern: /["'`](\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})["'`]/g,
|
|
34
|
+
guideline: 'Guideline 2.5.1 - IPv6 Compatibility',
|
|
35
|
+
suggestion: 'Use hostnames instead of hardcoded IP addresses for IPv6 compatibility.',
|
|
36
|
+
},
|
|
37
|
+
// Hardcoded API keys and secrets
|
|
38
|
+
{
|
|
39
|
+
id: 'hardcoded-api-key',
|
|
40
|
+
title: 'Potential hardcoded API key',
|
|
41
|
+
description: 'This appears to be a hardcoded API key or secret.',
|
|
42
|
+
severity: 'error',
|
|
43
|
+
pattern:
|
|
44
|
+
/(?:api[_-]?key|apikey|secret[_-]?key|secretkey|auth[_-]?token|access[_-]?token|private[_-]?key)\s*[:=]\s*["'`][A-Za-z0-9_\-]{16,}["'`]/gi,
|
|
45
|
+
guideline: 'Security Best Practice',
|
|
46
|
+
suggestion:
|
|
47
|
+
'Store sensitive keys in environment variables, Keychain, or a secure configuration system.',
|
|
48
|
+
},
|
|
49
|
+
// AWS keys
|
|
50
|
+
{
|
|
51
|
+
id: 'aws-key',
|
|
52
|
+
title: 'Potential AWS access key',
|
|
53
|
+
description: 'This appears to be a hardcoded AWS access key.',
|
|
54
|
+
severity: 'error',
|
|
55
|
+
pattern: /["'`](AKIA[0-9A-Z]{16})["'`]/g,
|
|
56
|
+
guideline: 'Security Best Practice',
|
|
57
|
+
suggestion: 'Never commit AWS keys to source code. Use IAM roles or secure key management.',
|
|
58
|
+
},
|
|
59
|
+
// Test/Debug server URLs
|
|
60
|
+
{
|
|
61
|
+
id: 'test-server-url',
|
|
62
|
+
title: 'Test/staging server URL',
|
|
63
|
+
description: 'This appears to be a test or staging server URL that should not be in production.',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
pattern:
|
|
66
|
+
/["'`]https?:\/\/(?:localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+|staging\.|test\.|dev\.)[^"'`]*["'`]/gi,
|
|
67
|
+
suggestion:
|
|
68
|
+
'Ensure test/staging URLs are not used in release builds. Use build configurations or environment variables.',
|
|
69
|
+
},
|
|
70
|
+
// Print/NSLog statements
|
|
71
|
+
{
|
|
72
|
+
id: 'print-statement',
|
|
73
|
+
title: 'Print/logging statement',
|
|
74
|
+
description: 'Console logging statements should be removed or disabled in release builds.',
|
|
75
|
+
severity: 'info',
|
|
76
|
+
pattern: /\b(?:print|NSLog|debugPrint)\s*\(/g,
|
|
77
|
+
suggestion: 'Use conditional logging that is disabled in release builds.',
|
|
78
|
+
fileTypes: ['.swift', '.m', '.mm'],
|
|
79
|
+
},
|
|
80
|
+
// TODO/FIXME comments
|
|
81
|
+
{
|
|
82
|
+
id: 'todo-comment',
|
|
83
|
+
title: 'TODO/FIXME comment',
|
|
84
|
+
description: 'Incomplete task marker found. Ensure all TODOs are addressed before release.',
|
|
85
|
+
severity: 'info',
|
|
86
|
+
pattern: /\/\/\s*(?:TODO|FIXME|HACK|XXX|BUG)[\s:]/gi,
|
|
87
|
+
suggestion: 'Address or remove TODO/FIXME comments before App Store submission.',
|
|
88
|
+
},
|
|
89
|
+
// Force unwrapping
|
|
90
|
+
{
|
|
91
|
+
id: 'force-unwrap',
|
|
92
|
+
title: 'Force unwrap operator',
|
|
93
|
+
description: 'Force unwrapping (!) can cause crashes if the value is nil.',
|
|
94
|
+
severity: 'info',
|
|
95
|
+
pattern: /\w+\s*!\s*(?:\.|$|\)|\])/g,
|
|
96
|
+
suggestion: 'Consider using optional binding (if let) or nil-coalescing (??) instead.',
|
|
97
|
+
fileTypes: ['.swift'],
|
|
98
|
+
},
|
|
99
|
+
// Hardcoded credentials
|
|
100
|
+
{
|
|
101
|
+
id: 'hardcoded-password',
|
|
102
|
+
title: 'Potential hardcoded password',
|
|
103
|
+
description: 'This appears to be a hardcoded password or credential.',
|
|
104
|
+
severity: 'error',
|
|
105
|
+
pattern: /(?:password|passwd|pwd|credential)\s*[:=]\s*["'`][^"'`]{4,}["'`]/gi,
|
|
106
|
+
guideline: 'Security Best Practice',
|
|
107
|
+
suggestion: 'Never hardcode passwords. Use Keychain or secure credential storage.',
|
|
108
|
+
},
|
|
109
|
+
// HTTP URLs (non-HTTPS)
|
|
110
|
+
{
|
|
111
|
+
id: 'insecure-http',
|
|
112
|
+
title: 'Insecure HTTP URL',
|
|
113
|
+
description: 'HTTP URLs are insecure. Use HTTPS for all network connections.',
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
pattern: /["'`]http:\/\/(?!localhost|127\.0\.0\.1)[^"'`]+["'`]/gi,
|
|
116
|
+
guideline: 'Guideline 2.5.4 - Security',
|
|
117
|
+
suggestion: 'Use HTTPS for all external URLs. Configure App Transport Security appropriately.',
|
|
118
|
+
},
|
|
119
|
+
// Placeholder text
|
|
120
|
+
{
|
|
121
|
+
id: 'placeholder-text',
|
|
122
|
+
title: 'Placeholder text',
|
|
123
|
+
description: 'Lorem ipsum or placeholder text detected.',
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
pattern: /["'`](?:lorem\s+ipsum|placeholder|sample\s+text|dummy\s+text)[^"'`]*["'`]/gi,
|
|
126
|
+
guideline: 'Guideline 2.3 - Accurate Metadata',
|
|
127
|
+
suggestion: 'Replace placeholder text with actual content before submission.',
|
|
128
|
+
},
|
|
129
|
+
// #if DEBUG with potentially problematic code
|
|
130
|
+
{
|
|
131
|
+
id: 'debug-ifdef',
|
|
132
|
+
title: '#if DEBUG block',
|
|
133
|
+
description: 'Debug-only code block detected. Verify it does not affect release functionality.',
|
|
134
|
+
severity: 'info',
|
|
135
|
+
pattern: /#if\s+DEBUG/g,
|
|
136
|
+
suggestion: 'Review DEBUG blocks to ensure they do not contain required functionality.',
|
|
137
|
+
fileTypes: ['.swift'],
|
|
138
|
+
},
|
|
139
|
+
// Deprecated UIWebView
|
|
140
|
+
{
|
|
141
|
+
id: 'deprecated-uiwebview',
|
|
142
|
+
title: 'Deprecated UIWebView usage',
|
|
143
|
+
description:
|
|
144
|
+
'UIWebView is deprecated and Apple rejects new apps using it. Use WKWebView instead.',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
pattern: /\bUIWebView\b/g,
|
|
147
|
+
guideline: 'ITMS-90809',
|
|
148
|
+
suggestion: 'Migrate to WKWebView. UIWebView is no longer accepted.',
|
|
149
|
+
},
|
|
150
|
+
// Deprecated APIs
|
|
151
|
+
{
|
|
152
|
+
id: 'deprecated-addressbook',
|
|
153
|
+
title: 'Deprecated AddressBook framework',
|
|
154
|
+
description: 'AddressBook framework is deprecated. Use Contacts framework instead.',
|
|
155
|
+
severity: 'warning',
|
|
156
|
+
pattern: /\bABAddressBook\w*\b/g,
|
|
157
|
+
suggestion: 'Migrate to the Contacts framework.',
|
|
158
|
+
fileTypes: ['.swift', '.m', '.mm'],
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Code scanner for detecting common issues
|
|
164
|
+
*/
|
|
165
|
+
export class CodeScanner implements Analyzer {
|
|
166
|
+
name = 'Code Scanner';
|
|
167
|
+
description = 'Scans source code for common App Store rejection issues';
|
|
168
|
+
|
|
169
|
+
async analyze(project: XcodeProject, options: AnalyzerOptions): Promise<AnalysisResult> {
|
|
170
|
+
const startTime = Date.now();
|
|
171
|
+
|
|
172
|
+
// Get source files from targets or scan directory
|
|
173
|
+
const targets = options.targetName
|
|
174
|
+
? project.targets.filter((t) => t.name === options.targetName)
|
|
175
|
+
: project.targets.filter((t) => t.type === 'application');
|
|
176
|
+
|
|
177
|
+
let sourceFiles: string[] = [];
|
|
178
|
+
for (const target of targets) {
|
|
179
|
+
sourceFiles.push(...target.sourceFiles);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If no source files from project, scan directory
|
|
183
|
+
if (sourceFiles.length === 0) {
|
|
184
|
+
sourceFiles = await this.findSourceFiles(options.basePath);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Filter to changed files for incremental scanning
|
|
188
|
+
if (options.changedFiles) {
|
|
189
|
+
const changedSet = new Set(options.changedFiles);
|
|
190
|
+
sourceFiles = sourceFiles.filter((f) => changedSet.has(f));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const issues = await this.scanFiles(sourceFiles);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
analyzer: this.name,
|
|
197
|
+
passed: issues.filter((i) => i.severity === 'error').length === 0,
|
|
198
|
+
issues,
|
|
199
|
+
duration: Date.now() - startTime,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Scan a specific path (file or directory)
|
|
205
|
+
*/
|
|
206
|
+
async scanPath(scanPath: string, patterns?: string[]): Promise<AnalysisResult> {
|
|
207
|
+
const startTime = Date.now();
|
|
208
|
+
|
|
209
|
+
const stats = await fs.stat(scanPath);
|
|
210
|
+
const files = stats.isDirectory()
|
|
211
|
+
? await this.findSourceFiles(scanPath)
|
|
212
|
+
: [scanPath];
|
|
213
|
+
|
|
214
|
+
const activePatterns = patterns
|
|
215
|
+
? SCAN_PATTERNS.filter((p) => patterns.includes(p.id))
|
|
216
|
+
: SCAN_PATTERNS;
|
|
217
|
+
|
|
218
|
+
const issues = await this.scanFiles(files, activePatterns);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
analyzer: this.name,
|
|
222
|
+
passed: issues.filter((i) => i.severity === 'error').length === 0,
|
|
223
|
+
issues,
|
|
224
|
+
duration: Date.now() - startTime,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Find all source files in a directory
|
|
230
|
+
*/
|
|
231
|
+
private async findSourceFiles(basePath: string): Promise<string[]> {
|
|
232
|
+
return fg(['**/*.swift', '**/*.m', '**/*.mm', '**/*.h'], {
|
|
233
|
+
cwd: basePath,
|
|
234
|
+
absolute: true,
|
|
235
|
+
ignore: [
|
|
236
|
+
'**/Pods/**',
|
|
237
|
+
'**/Carthage/**',
|
|
238
|
+
'**/build/**',
|
|
239
|
+
'**/DerivedData/**',
|
|
240
|
+
'**/*.generated.swift',
|
|
241
|
+
'**/Tests/**',
|
|
242
|
+
'**/UITests/**',
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Scan files for issues
|
|
249
|
+
*/
|
|
250
|
+
private async scanFiles(
|
|
251
|
+
files: string[],
|
|
252
|
+
patterns: ScanPattern[] = SCAN_PATTERNS
|
|
253
|
+
): Promise<Issue[]> {
|
|
254
|
+
const issues: Issue[] = [];
|
|
255
|
+
const seenIssues = new Set<string>();
|
|
256
|
+
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
const ext = path.extname(file);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
262
|
+
const lines = content.split('\n');
|
|
263
|
+
|
|
264
|
+
for (const pattern of patterns) {
|
|
265
|
+
// Skip if pattern is for specific file types and this isn't one
|
|
266
|
+
if (pattern.fileTypes && !pattern.fileTypes.includes(ext)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Reset regex state
|
|
271
|
+
pattern.pattern.lastIndex = 0;
|
|
272
|
+
|
|
273
|
+
let match: RegExpExecArray | null;
|
|
274
|
+
while ((match = pattern.pattern.exec(content)) !== null) {
|
|
275
|
+
// Find line number
|
|
276
|
+
const lineNumber = this.getLineNumber(content, match.index);
|
|
277
|
+
const issueKey = `${pattern.id}:${file}:${lineNumber}`;
|
|
278
|
+
|
|
279
|
+
// Avoid duplicate issues at the same location
|
|
280
|
+
if (seenIssues.has(issueKey)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
seenIssues.add(issueKey);
|
|
284
|
+
|
|
285
|
+
// Skip false positives
|
|
286
|
+
if (this.isFalsePositive(pattern.id, match[0], lines[lineNumber - 1] ?? '')) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const issue: Issue = {
|
|
291
|
+
id: pattern.id,
|
|
292
|
+
title: pattern.title,
|
|
293
|
+
description: `${pattern.description}\n\nFound: \`${this.truncate(match[0], 50)}\``,
|
|
294
|
+
severity: pattern.severity,
|
|
295
|
+
filePath: file,
|
|
296
|
+
lineNumber,
|
|
297
|
+
category: 'code',
|
|
298
|
+
};
|
|
299
|
+
if (pattern.guideline) {
|
|
300
|
+
issue.guideline = pattern.guideline;
|
|
301
|
+
}
|
|
302
|
+
if (pattern.suggestion) {
|
|
303
|
+
issue.suggestion = pattern.suggestion;
|
|
304
|
+
}
|
|
305
|
+
issues.push(issue);
|
|
306
|
+
|
|
307
|
+
// Limit issues per pattern per file
|
|
308
|
+
const issuesForPattern = issues.filter(
|
|
309
|
+
(i) => i.id === pattern.id && i.filePath === file
|
|
310
|
+
);
|
|
311
|
+
if (issuesForPattern.length >= 5) {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// Skip files that can't be read
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return issues;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get line number from character index
|
|
326
|
+
*/
|
|
327
|
+
private getLineNumber(content: string, index: number): number {
|
|
328
|
+
return content.substring(0, index).split('\n').length;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Check if a match is a false positive
|
|
333
|
+
*/
|
|
334
|
+
private isFalsePositive(patternId: string, match: string, line: string): boolean {
|
|
335
|
+
// Skip commented lines
|
|
336
|
+
const trimmedLine = line.trim();
|
|
337
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || trimmedLine.startsWith('/*')) {
|
|
338
|
+
// Allow TODO/FIXME in comments (that's what we're looking for)
|
|
339
|
+
if (patternId !== 'todo-comment') {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Skip test files content
|
|
345
|
+
if (line.includes('XCTest') || line.includes('@testable')) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Pattern-specific false positive handling
|
|
350
|
+
switch (patternId) {
|
|
351
|
+
case 'hardcoded-ipv4':
|
|
352
|
+
// Skip version numbers that look like IPs
|
|
353
|
+
if (/\d+\.\d+\.\d+\.\d+/.test(match)) {
|
|
354
|
+
const ip = match.match(/\d+\.\d+\.\d+\.\d+/)?.[0];
|
|
355
|
+
if (ip) {
|
|
356
|
+
const parts = ip.split('.').map(Number);
|
|
357
|
+
// Skip if any part > 255 (not a valid IP)
|
|
358
|
+
if (parts.some((p) => p !== undefined && p > 255)) {
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
// Skip localhost
|
|
362
|
+
if (ip === '127.0.0.1' || ip === '0.0.0.0') {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
case 'force-unwrap':
|
|
370
|
+
// Skip IBOutlets and known safe patterns
|
|
371
|
+
if (line.includes('@IBOutlet') || line.includes('@IBAction')) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
// Skip try! and as!
|
|
375
|
+
if (/try\s*!/.test(match) || /as\s*!/.test(match)) {
|
|
376
|
+
// These are separate issues, not force unwrap
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
break;
|
|
380
|
+
|
|
381
|
+
case 'print-statement':
|
|
382
|
+
// Skip if inside #if DEBUG
|
|
383
|
+
if (line.includes('#if DEBUG') || line.includes('#if debug')) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
case 'insecure-http':
|
|
389
|
+
// Skip App Transport Security exception domains
|
|
390
|
+
if (line.includes('NSExceptionDomains') || line.includes('Exception')) {
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Truncate string for display
|
|
401
|
+
*/
|
|
402
|
+
private truncate(str: string, maxLength: number): string {
|
|
403
|
+
if (str.length <= maxLength) {
|
|
404
|
+
return str;
|
|
405
|
+
}
|
|
406
|
+
return str.substring(0, maxLength - 3) + '...';
|
|
407
|
+
}
|
|
408
|
+
}
|