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,745 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { CodeScanner } from '../../src/analyzers/code-scanner.js';
|
|
5
|
+
import type { XcodeProject, AnalyzerOptions } from '../../src/types/index.js';
|
|
6
|
+
|
|
7
|
+
describe('CodeScanner', () => {
|
|
8
|
+
let scanner: CodeScanner;
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'code-scanner-test-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
scanner = new CodeScanner();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function makeMockProject(overrides?: {
|
|
25
|
+
sourceFiles?: string[];
|
|
26
|
+
targets?: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
type: 'application' | 'framework';
|
|
29
|
+
sourceFiles: string[];
|
|
30
|
+
}>;
|
|
31
|
+
}): XcodeProject {
|
|
32
|
+
const targets = overrides?.targets ?? [
|
|
33
|
+
{
|
|
34
|
+
name: 'TestApp',
|
|
35
|
+
type: 'application' as const,
|
|
36
|
+
bundleIdentifier: 'com.test.app',
|
|
37
|
+
deploymentTarget: '16.0',
|
|
38
|
+
sourceFiles: overrides?.sourceFiles ?? [],
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
return {
|
|
42
|
+
path: '/test/TestApp.xcodeproj',
|
|
43
|
+
name: 'TestApp',
|
|
44
|
+
targets: targets.map((t) => ({
|
|
45
|
+
bundleIdentifier: 'com.test.app',
|
|
46
|
+
deploymentTarget: '16.0',
|
|
47
|
+
...t,
|
|
48
|
+
})),
|
|
49
|
+
configurations: ['Debug', 'Release'],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeOptions(overrides?: Partial<AnalyzerOptions>): AnalyzerOptions {
|
|
54
|
+
return {
|
|
55
|
+
basePath: tempDir,
|
|
56
|
+
...overrides,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('analyze - pattern detection', () => {
|
|
61
|
+
let sourceDir: string;
|
|
62
|
+
let swiftFile: string;
|
|
63
|
+
|
|
64
|
+
beforeAll(async () => {
|
|
65
|
+
sourceDir = path.join(tempDir, 'patterns');
|
|
66
|
+
await fs.mkdir(sourceDir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
swiftFile = path.join(sourceDir, 'ViewController.swift');
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
swiftFile,
|
|
71
|
+
`import UIKit
|
|
72
|
+
import WebKit
|
|
73
|
+
|
|
74
|
+
class ViewController: UIViewController {
|
|
75
|
+
let apiKey = "sk-test1234567890abcdef"
|
|
76
|
+
let awsKey = "AKIAIOSFODNN7EXAMPLE"
|
|
77
|
+
let url = "http://api.example.com/data"
|
|
78
|
+
let password = "mysecretpassword"
|
|
79
|
+
|
|
80
|
+
override func viewDidLoad() {
|
|
81
|
+
super.viewDidLoad()
|
|
82
|
+
print("Debug: loaded")
|
|
83
|
+
NSLog("Debug log")
|
|
84
|
+
debugPrint("verbose")
|
|
85
|
+
|
|
86
|
+
let value = someOptional!.property
|
|
87
|
+
let result = try! someThrowingFunc()
|
|
88
|
+
|
|
89
|
+
// TODO: fix this before release
|
|
90
|
+
// FIXME: hack that needs cleanup
|
|
91
|
+
|
|
92
|
+
let webView = UIWebView()
|
|
93
|
+
let ipsum = "lorem ipsum dolor sit amet"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`,
|
|
97
|
+
'utf-8'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should detect hardcoded AWS keys', async () => {
|
|
102
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
103
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
104
|
+
|
|
105
|
+
const awsIssue = result.issues.find((i) => i.id === 'aws-key');
|
|
106
|
+
expect(awsIssue).toBeDefined();
|
|
107
|
+
expect(awsIssue?.severity).toBe('error');
|
|
108
|
+
expect(awsIssue?.filePath).toBe(swiftFile);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should detect print/NSLog/debugPrint statements', async () => {
|
|
112
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
113
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
114
|
+
|
|
115
|
+
const printIssues = result.issues.filter((i) => i.id === 'print-statement');
|
|
116
|
+
expect(printIssues.length).toBeGreaterThanOrEqual(1);
|
|
117
|
+
expect(printIssues[0]?.severity).toBe('info');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should detect force unwrap usage', async () => {
|
|
121
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
122
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
123
|
+
|
|
124
|
+
const forceUnwrap = result.issues.find((i) => i.id === 'force-unwrap');
|
|
125
|
+
expect(forceUnwrap).toBeDefined();
|
|
126
|
+
expect(forceUnwrap?.severity).toBe('info');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should detect deprecated UIWebView usage', async () => {
|
|
130
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
131
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
132
|
+
|
|
133
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
134
|
+
expect(webViewIssue).toBeDefined();
|
|
135
|
+
expect(webViewIssue?.severity).toBe('error');
|
|
136
|
+
expect(webViewIssue?.guideline).toBe('ITMS-90809');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should detect insecure HTTP URLs', async () => {
|
|
140
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
141
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
142
|
+
|
|
143
|
+
const httpIssue = result.issues.find((i) => i.id === 'insecure-http');
|
|
144
|
+
expect(httpIssue).toBeDefined();
|
|
145
|
+
expect(httpIssue?.severity).toBe('warning');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should detect TODO/FIXME comments', async () => {
|
|
149
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
150
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
151
|
+
|
|
152
|
+
const todoIssues = result.issues.filter((i) => i.id === 'todo-comment');
|
|
153
|
+
expect(todoIssues.length).toBeGreaterThanOrEqual(2);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should detect placeholder text', async () => {
|
|
157
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
158
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
159
|
+
|
|
160
|
+
const placeholderIssue = result.issues.find((i) => i.id === 'placeholder-text');
|
|
161
|
+
expect(placeholderIssue).toBeDefined();
|
|
162
|
+
expect(placeholderIssue?.severity).toBe('warning');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should detect hardcoded passwords', async () => {
|
|
166
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
167
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
168
|
+
|
|
169
|
+
const pwIssue = result.issues.find((i) => i.id === 'hardcoded-password');
|
|
170
|
+
expect(pwIssue).toBeDefined();
|
|
171
|
+
expect(pwIssue?.severity).toBe('error');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should mark result as not passed when errors exist', async () => {
|
|
175
|
+
const project = makeMockProject({ sourceFiles: [swiftFile] });
|
|
176
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: sourceDir }));
|
|
177
|
+
|
|
178
|
+
expect(result.passed).toBe(false);
|
|
179
|
+
expect(result.issues.some((i) => i.severity === 'error')).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('analyze - ObjC patterns', () => {
|
|
184
|
+
let objcDir: string;
|
|
185
|
+
let objcFile: string;
|
|
186
|
+
|
|
187
|
+
beforeAll(async () => {
|
|
188
|
+
objcDir = path.join(tempDir, 'objc-patterns');
|
|
189
|
+
await fs.mkdir(objcDir, { recursive: true });
|
|
190
|
+
|
|
191
|
+
objcFile = path.join(objcDir, 'LegacyController.m');
|
|
192
|
+
await fs.writeFile(
|
|
193
|
+
objcFile,
|
|
194
|
+
`#import <UIKit/UIKit.h>
|
|
195
|
+
#import <AddressBook/AddressBook.h>
|
|
196
|
+
|
|
197
|
+
@implementation LegacyController
|
|
198
|
+
|
|
199
|
+
- (void)viewDidLoad {
|
|
200
|
+
[super viewDidLoad];
|
|
201
|
+
NSLog(@"View loaded");
|
|
202
|
+
UIWebView *webView = [[UIWebView alloc] init];
|
|
203
|
+
ABAddressBookRef addressBook = ABAddressBookCreate();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@end
|
|
207
|
+
`,
|
|
208
|
+
'utf-8'
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should detect UIWebView in ObjC files', async () => {
|
|
213
|
+
const project = makeMockProject({ sourceFiles: [objcFile] });
|
|
214
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: objcDir }));
|
|
215
|
+
|
|
216
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
217
|
+
expect(webViewIssue).toBeDefined();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should detect deprecated AddressBook framework', async () => {
|
|
221
|
+
const project = makeMockProject({ sourceFiles: [objcFile] });
|
|
222
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: objcDir }));
|
|
223
|
+
|
|
224
|
+
const abIssue = result.issues.find((i) => i.id === 'deprecated-addressbook');
|
|
225
|
+
expect(abIssue).toBeDefined();
|
|
226
|
+
expect(abIssue?.severity).toBe('warning');
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('analyze - hardcoded IPv4 and test server URLs', () => {
|
|
231
|
+
let ipDir: string;
|
|
232
|
+
let ipFile: string;
|
|
233
|
+
|
|
234
|
+
beforeAll(async () => {
|
|
235
|
+
ipDir = path.join(tempDir, 'ip-patterns');
|
|
236
|
+
await fs.mkdir(ipDir, { recursive: true });
|
|
237
|
+
|
|
238
|
+
ipFile = path.join(ipDir, 'Network.swift');
|
|
239
|
+
await fs.writeFile(
|
|
240
|
+
ipFile,
|
|
241
|
+
`import Foundation
|
|
242
|
+
|
|
243
|
+
let serverIP = "192.168.1.100"
|
|
244
|
+
let testURL = "http://staging.example.com/api"
|
|
245
|
+
let debugURL = "http://localhost:8080/test"
|
|
246
|
+
let devServer = "http://dev.myapp.com/endpoint"
|
|
247
|
+
`,
|
|
248
|
+
'utf-8'
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should detect hardcoded IPv4 addresses', async () => {
|
|
253
|
+
const project = makeMockProject({ sourceFiles: [ipFile] });
|
|
254
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: ipDir }));
|
|
255
|
+
|
|
256
|
+
const ipIssue = result.issues.find((i) => i.id === 'hardcoded-ipv4');
|
|
257
|
+
expect(ipIssue).toBeDefined();
|
|
258
|
+
expect(ipIssue?.severity).toBe('warning');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should detect test/staging server URLs', async () => {
|
|
262
|
+
const project = makeMockProject({ sourceFiles: [ipFile] });
|
|
263
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: ipDir }));
|
|
264
|
+
|
|
265
|
+
const testUrlIssues = result.issues.filter((i) => i.id === 'test-server-url');
|
|
266
|
+
expect(testUrlIssues.length).toBeGreaterThanOrEqual(1);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('analyze - debug ifdef', () => {
|
|
271
|
+
it('should detect #if DEBUG blocks in Swift files', async () => {
|
|
272
|
+
const subDir = path.join(tempDir, 'debug-ifdef');
|
|
273
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
const filePath = path.join(subDir, 'Config.swift');
|
|
276
|
+
await fs.writeFile(
|
|
277
|
+
filePath,
|
|
278
|
+
`import Foundation
|
|
279
|
+
|
|
280
|
+
#if DEBUG
|
|
281
|
+
let baseURL = "http://localhost:3000"
|
|
282
|
+
#else
|
|
283
|
+
let baseURL = "https://api.production.com"
|
|
284
|
+
#endif
|
|
285
|
+
`,
|
|
286
|
+
'utf-8'
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
290
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
291
|
+
|
|
292
|
+
const debugIssue = result.issues.find((i) => i.id === 'debug-ifdef');
|
|
293
|
+
expect(debugIssue).toBeDefined();
|
|
294
|
+
expect(debugIssue?.severity).toBe('info');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('analyze - hardcoded API key pattern', () => {
|
|
299
|
+
it('should detect api_key = "..." pattern', async () => {
|
|
300
|
+
const subDir = path.join(tempDir, 'apikey-pattern');
|
|
301
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
302
|
+
|
|
303
|
+
const filePath = path.join(subDir, 'Keys.swift');
|
|
304
|
+
await fs.writeFile(
|
|
305
|
+
filePath,
|
|
306
|
+
`import Foundation
|
|
307
|
+
|
|
308
|
+
let api_key = "abcdef1234567890abcdef"
|
|
309
|
+
let secret_key = "secretvalue12345678901234"
|
|
310
|
+
`,
|
|
311
|
+
'utf-8'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
315
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
316
|
+
|
|
317
|
+
const apiKeyIssue = result.issues.find((i) => i.id === 'hardcoded-api-key');
|
|
318
|
+
expect(apiKeyIssue).toBeDefined();
|
|
319
|
+
expect(apiKeyIssue?.severity).toBe('error');
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('analyze - false positive handling', () => {
|
|
324
|
+
it('should skip patterns in commented-out lines', async () => {
|
|
325
|
+
const subDir = path.join(tempDir, 'false-positive-comments');
|
|
326
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const filePath = path.join(subDir, 'Commented.swift');
|
|
329
|
+
await fs.writeFile(
|
|
330
|
+
filePath,
|
|
331
|
+
`import Foundation
|
|
332
|
+
|
|
333
|
+
// let awsKey = "AKIAIOSFODNN7EXAMPLE"
|
|
334
|
+
/* password = "mysecret1234" */
|
|
335
|
+
* let webView = UIWebView()
|
|
336
|
+
`,
|
|
337
|
+
'utf-8'
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
341
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
342
|
+
|
|
343
|
+
// Commented-out patterns should be filtered as false positives
|
|
344
|
+
// (except TODO/FIXME which are expected in comments)
|
|
345
|
+
const awsIssue = result.issues.find((i) => i.id === 'aws-key');
|
|
346
|
+
expect(awsIssue).toBeUndefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should not flag @IBOutlet force unwraps as issues', async () => {
|
|
350
|
+
const subDir = path.join(tempDir, 'false-positive-iboutlet');
|
|
351
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
352
|
+
|
|
353
|
+
const filePath = path.join(subDir, 'Outlet.swift');
|
|
354
|
+
await fs.writeFile(
|
|
355
|
+
filePath,
|
|
356
|
+
`import UIKit
|
|
357
|
+
|
|
358
|
+
class MyVC: UIViewController {
|
|
359
|
+
@IBOutlet weak var label: UILabel!
|
|
360
|
+
@IBAction func tapped(_ sender: UIButton!) {
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
`,
|
|
364
|
+
'utf-8'
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
368
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
369
|
+
|
|
370
|
+
// IBOutlet/IBAction force unwraps should be treated as false positives
|
|
371
|
+
const forceUnwrapIssues = result.issues.filter(
|
|
372
|
+
(i) => i.id === 'force-unwrap' && i.filePath === filePath
|
|
373
|
+
);
|
|
374
|
+
expect(forceUnwrapIssues).toHaveLength(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should skip lines containing XCTest as false positives', async () => {
|
|
378
|
+
const subDir = path.join(tempDir, 'false-positive-xctest');
|
|
379
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
380
|
+
|
|
381
|
+
const filePath = path.join(subDir, 'HelperForTest.swift');
|
|
382
|
+
await fs.writeFile(
|
|
383
|
+
filePath,
|
|
384
|
+
`import Foundation
|
|
385
|
+
import XCTest let password = "testpassword1234"
|
|
386
|
+
`,
|
|
387
|
+
'utf-8'
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
391
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
392
|
+
|
|
393
|
+
// The XCTest line should be filtered out
|
|
394
|
+
const pwIssue = result.issues.find(
|
|
395
|
+
(i) => i.id === 'hardcoded-password' && i.filePath === filePath
|
|
396
|
+
);
|
|
397
|
+
expect(pwIssue).toBeUndefined();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should not flag insecure HTTP URLs next to NSExceptionDomains', async () => {
|
|
401
|
+
const subDir = path.join(tempDir, 'false-positive-ats');
|
|
402
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
403
|
+
|
|
404
|
+
const filePath = path.join(subDir, 'ATS.swift');
|
|
405
|
+
await fs.writeFile(
|
|
406
|
+
filePath,
|
|
407
|
+
`import Foundation
|
|
408
|
+
let domain = "http://example.com" // NSExceptionDomains
|
|
409
|
+
`,
|
|
410
|
+
'utf-8'
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
414
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
415
|
+
|
|
416
|
+
const httpIssue = result.issues.find(
|
|
417
|
+
(i) => i.id === 'insecure-http' && i.filePath === filePath
|
|
418
|
+
);
|
|
419
|
+
expect(httpIssue).toBeUndefined();
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('analyze - file type filtering', () => {
|
|
424
|
+
it('should not flag print statements in .h header files', async () => {
|
|
425
|
+
const subDir = path.join(tempDir, 'filetype-filter');
|
|
426
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
427
|
+
|
|
428
|
+
const headerFile = path.join(subDir, 'Header.h');
|
|
429
|
+
await fs.writeFile(
|
|
430
|
+
headerFile,
|
|
431
|
+
`// Header file
|
|
432
|
+
void print(const char *msg);
|
|
433
|
+
`,
|
|
434
|
+
'utf-8'
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const project = makeMockProject({ sourceFiles: [headerFile] });
|
|
438
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
439
|
+
|
|
440
|
+
// print-statement pattern has fileTypes: ['.swift', '.m', '.mm']
|
|
441
|
+
// .h files should not match
|
|
442
|
+
const printIssues = result.issues.filter(
|
|
443
|
+
(i) => i.id === 'print-statement' && i.filePath === headerFile
|
|
444
|
+
);
|
|
445
|
+
expect(printIssues).toHaveLength(0);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
describe('analyze - glob fallback and target filtering', () => {
|
|
450
|
+
it('should find source files via glob when no source files in target', async () => {
|
|
451
|
+
const subDir = path.join(tempDir, 'glob-fallback');
|
|
452
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
453
|
+
|
|
454
|
+
await fs.writeFile(
|
|
455
|
+
path.join(subDir, 'Found.swift'),
|
|
456
|
+
`import UIKit
|
|
457
|
+
let webView = UIWebView()
|
|
458
|
+
`,
|
|
459
|
+
'utf-8'
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const project = makeMockProject({ sourceFiles: [] });
|
|
463
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
464
|
+
|
|
465
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
466
|
+
expect(webViewIssue).toBeDefined();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should filter targets by options.targetName', async () => {
|
|
470
|
+
const subDir = path.join(tempDir, 'target-name-filter');
|
|
471
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
472
|
+
|
|
473
|
+
const mainFile = path.join(subDir, 'Main.swift');
|
|
474
|
+
await fs.writeFile(mainFile, 'let webView = UIWebView()\n', 'utf-8');
|
|
475
|
+
|
|
476
|
+
const extensionFile = path.join(subDir, 'Extension.swift');
|
|
477
|
+
await fs.writeFile(extensionFile, 'import Foundation\n', 'utf-8');
|
|
478
|
+
|
|
479
|
+
const project: XcodeProject = {
|
|
480
|
+
path: '/test/TestApp.xcodeproj',
|
|
481
|
+
name: 'TestApp',
|
|
482
|
+
targets: [
|
|
483
|
+
{
|
|
484
|
+
name: 'MainApp',
|
|
485
|
+
type: 'application',
|
|
486
|
+
bundleIdentifier: 'com.test.main',
|
|
487
|
+
deploymentTarget: '16.0',
|
|
488
|
+
sourceFiles: [mainFile],
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
name: 'CleanExtension',
|
|
492
|
+
type: 'appExtension',
|
|
493
|
+
bundleIdentifier: 'com.test.ext',
|
|
494
|
+
deploymentTarget: '16.0',
|
|
495
|
+
sourceFiles: [extensionFile],
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
configurations: ['Debug', 'Release'],
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// Only scan CleanExtension
|
|
502
|
+
const result = await scanner.analyze(project, makeOptions({
|
|
503
|
+
basePath: subDir,
|
|
504
|
+
targetName: 'CleanExtension',
|
|
505
|
+
}));
|
|
506
|
+
|
|
507
|
+
// CleanExtension has a clean file, MainApp's UIWebView should not appear
|
|
508
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
509
|
+
expect(webViewIssue).toBeUndefined();
|
|
510
|
+
expect(result.passed).toBe(true);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should process multiple targets when no targetName specified', async () => {
|
|
514
|
+
const subDir = path.join(tempDir, 'multi-target');
|
|
515
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
516
|
+
|
|
517
|
+
const file1 = path.join(subDir, 'App1.swift');
|
|
518
|
+
await fs.writeFile(file1, 'let webView = UIWebView()\n', 'utf-8');
|
|
519
|
+
|
|
520
|
+
const file2 = path.join(subDir, 'App2.swift');
|
|
521
|
+
await fs.writeFile(file2, 'let key = "AKIAIOSFODNN7EXAMPLE"\n', 'utf-8');
|
|
522
|
+
|
|
523
|
+
const project: XcodeProject = {
|
|
524
|
+
path: '/test/TestApp.xcodeproj',
|
|
525
|
+
name: 'TestApp',
|
|
526
|
+
targets: [
|
|
527
|
+
{
|
|
528
|
+
name: 'App1',
|
|
529
|
+
type: 'application',
|
|
530
|
+
bundleIdentifier: 'com.test.app1',
|
|
531
|
+
deploymentTarget: '16.0',
|
|
532
|
+
sourceFiles: [file1],
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: 'App2',
|
|
536
|
+
type: 'application',
|
|
537
|
+
bundleIdentifier: 'com.test.app2',
|
|
538
|
+
deploymentTarget: '16.0',
|
|
539
|
+
sourceFiles: [file2],
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
configurations: ['Debug', 'Release'],
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
546
|
+
|
|
547
|
+
// Both targets' files should be scanned
|
|
548
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
549
|
+
const awsIssue = result.issues.find((i) => i.id === 'aws-key');
|
|
550
|
+
expect(webViewIssue).toBeDefined();
|
|
551
|
+
expect(awsIssue).toBeDefined();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
describe('analyze - changedFiles filtering', () => {
|
|
556
|
+
it('should only scan files that are in the changedFiles set', async () => {
|
|
557
|
+
const subDir = path.join(tempDir, 'changed-files');
|
|
558
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
559
|
+
|
|
560
|
+
const changedFile = path.join(subDir, 'Changed.swift');
|
|
561
|
+
await fs.writeFile(changedFile, 'let webView = UIWebView()\n', 'utf-8');
|
|
562
|
+
|
|
563
|
+
const unchangedFile = path.join(subDir, 'Unchanged.swift');
|
|
564
|
+
await fs.writeFile(unchangedFile, 'let key = "AKIAIOSFODNN7EXAMPLE"\n', 'utf-8');
|
|
565
|
+
|
|
566
|
+
const project = makeMockProject({
|
|
567
|
+
sourceFiles: [changedFile, unchangedFile],
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const result = await scanner.analyze(project, makeOptions({
|
|
571
|
+
basePath: subDir,
|
|
572
|
+
changedFiles: [changedFile],
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
// Only changedFile should be scanned
|
|
576
|
+
const webViewIssue = result.issues.find((i) => i.id === 'deprecated-uiwebview');
|
|
577
|
+
expect(webViewIssue).toBeDefined();
|
|
578
|
+
|
|
579
|
+
// unchangedFile has AWS key but should not be scanned
|
|
580
|
+
const awsIssue = result.issues.find((i) => i.id === 'aws-key');
|
|
581
|
+
expect(awsIssue).toBeUndefined();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should return no issues when changedFiles is empty', async () => {
|
|
585
|
+
const subDir = path.join(tempDir, 'changed-empty');
|
|
586
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
587
|
+
|
|
588
|
+
const file = path.join(subDir, 'HasIssues.swift');
|
|
589
|
+
await fs.writeFile(file, 'let webView = UIWebView()\n', 'utf-8');
|
|
590
|
+
|
|
591
|
+
const project = makeMockProject({ sourceFiles: [file] });
|
|
592
|
+
|
|
593
|
+
const result = await scanner.analyze(project, makeOptions({
|
|
594
|
+
basePath: subDir,
|
|
595
|
+
changedFiles: [],
|
|
596
|
+
}));
|
|
597
|
+
|
|
598
|
+
expect(result.issues).toHaveLength(0);
|
|
599
|
+
expect(result.passed).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('scanPath', () => {
|
|
604
|
+
it('should scan a single file directly', async () => {
|
|
605
|
+
const subDir = path.join(tempDir, 'scanpath-file');
|
|
606
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
607
|
+
|
|
608
|
+
const filePath = path.join(subDir, 'Single.swift');
|
|
609
|
+
await fs.writeFile(
|
|
610
|
+
filePath,
|
|
611
|
+
`import UIKit
|
|
612
|
+
let webView = UIWebView()
|
|
613
|
+
let key = "AKIAIOSFODNN7EXAMPLE"
|
|
614
|
+
print("debug info")
|
|
615
|
+
`,
|
|
616
|
+
'utf-8'
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
const result = await scanner.scanPath(filePath);
|
|
620
|
+
|
|
621
|
+
expect(result.analyzer).toBe('Code Scanner');
|
|
622
|
+
expect(result.passed).toBe(false);
|
|
623
|
+
expect(result.issues.some((i) => i.id === 'deprecated-uiwebview')).toBe(true);
|
|
624
|
+
expect(result.issues.some((i) => i.id === 'aws-key')).toBe(true);
|
|
625
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('should scan a directory for all source files', async () => {
|
|
629
|
+
const subDir = path.join(tempDir, 'scanpath-dir');
|
|
630
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
631
|
+
|
|
632
|
+
await fs.writeFile(
|
|
633
|
+
path.join(subDir, 'FileA.swift'),
|
|
634
|
+
'let webView = UIWebView()\n',
|
|
635
|
+
'utf-8'
|
|
636
|
+
);
|
|
637
|
+
await fs.writeFile(
|
|
638
|
+
path.join(subDir, 'FileB.swift'),
|
|
639
|
+
'let key = "AKIAIOSFODNN7EXAMPLE"\n',
|
|
640
|
+
'utf-8'
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const result = await scanner.scanPath(subDir);
|
|
644
|
+
|
|
645
|
+
expect(result.issues.some((i) => i.id === 'deprecated-uiwebview')).toBe(true);
|
|
646
|
+
expect(result.issues.some((i) => i.id === 'aws-key')).toBe(true);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it('should filter patterns when pattern IDs are provided', async () => {
|
|
650
|
+
const subDir = path.join(tempDir, 'scanpath-patterns');
|
|
651
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
652
|
+
|
|
653
|
+
const filePath = path.join(subDir, 'Mixed.swift');
|
|
654
|
+
await fs.writeFile(
|
|
655
|
+
filePath,
|
|
656
|
+
`import UIKit
|
|
657
|
+
let webView = UIWebView()
|
|
658
|
+
let key = "AKIAIOSFODNN7EXAMPLE"
|
|
659
|
+
print("debug info")
|
|
660
|
+
// TODO: fix this
|
|
661
|
+
`,
|
|
662
|
+
'utf-8'
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// Only scan for deprecated-uiwebview
|
|
666
|
+
const result = await scanner.scanPath(filePath, ['deprecated-uiwebview']);
|
|
667
|
+
|
|
668
|
+
const webViewIssues = result.issues.filter((i) => i.id === 'deprecated-uiwebview');
|
|
669
|
+
expect(webViewIssues.length).toBeGreaterThanOrEqual(1);
|
|
670
|
+
|
|
671
|
+
// Other patterns should NOT be detected
|
|
672
|
+
const awsIssue = result.issues.find((i) => i.id === 'aws-key');
|
|
673
|
+
expect(awsIssue).toBeUndefined();
|
|
674
|
+
|
|
675
|
+
const printIssue = result.issues.find((i) => i.id === 'print-statement');
|
|
676
|
+
expect(printIssue).toBeUndefined();
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe('result metadata', () => {
|
|
681
|
+
it('should include analyzer name and duration', async () => {
|
|
682
|
+
const subDir = path.join(tempDir, 'metadata');
|
|
683
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
684
|
+
|
|
685
|
+
const filePath = path.join(subDir, 'Clean.swift');
|
|
686
|
+
await fs.writeFile(filePath, 'import Foundation\n', 'utf-8');
|
|
687
|
+
|
|
688
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
689
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
690
|
+
|
|
691
|
+
expect(result.analyzer).toBe('Code Scanner');
|
|
692
|
+
expect(result.duration).toBeGreaterThanOrEqual(0);
|
|
693
|
+
expect(result.passed).toBe(true);
|
|
694
|
+
expect(result.issues).toHaveLength(0);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should truncate long match strings in issue descriptions', async () => {
|
|
698
|
+
const subDir = path.join(tempDir, 'truncate');
|
|
699
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
700
|
+
|
|
701
|
+
const filePath = path.join(subDir, 'LongMatch.swift');
|
|
702
|
+
// Create a very long insecure URL that will exceed truncation limit
|
|
703
|
+
const longUrl = `"http://very-long-domain-name-that-goes-on-and-on-and-on-for-testing-purposes.example.com/some/really/long/path/that/keeps/going"`;
|
|
704
|
+
await fs.writeFile(
|
|
705
|
+
filePath,
|
|
706
|
+
`import Foundation
|
|
707
|
+
let url = ${longUrl}
|
|
708
|
+
`,
|
|
709
|
+
'utf-8'
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
713
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
714
|
+
|
|
715
|
+
const httpIssue = result.issues.find((i) => i.id === 'insecure-http');
|
|
716
|
+
expect(httpIssue).toBeDefined();
|
|
717
|
+
// Long matches get truncated with "..."
|
|
718
|
+
if (httpIssue && httpIssue.description.includes('...')) {
|
|
719
|
+
expect(httpIssue.description).toContain('...');
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
describe('issue limit per pattern per file', () => {
|
|
725
|
+
it('should limit issues per pattern to 5 per file', async () => {
|
|
726
|
+
const subDir = path.join(tempDir, 'issue-limit');
|
|
727
|
+
await fs.mkdir(subDir, { recursive: true });
|
|
728
|
+
|
|
729
|
+
const filePath = path.join(subDir, 'ManyTodos.swift');
|
|
730
|
+
const lines = ['import Foundation'];
|
|
731
|
+
for (let i = 0; i < 10; i++) {
|
|
732
|
+
lines.push(`// TODO: task ${i}`);
|
|
733
|
+
}
|
|
734
|
+
await fs.writeFile(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
735
|
+
|
|
736
|
+
const project = makeMockProject({ sourceFiles: [filePath] });
|
|
737
|
+
const result = await scanner.analyze(project, makeOptions({ basePath: subDir }));
|
|
738
|
+
|
|
739
|
+
const todoIssues = result.issues.filter(
|
|
740
|
+
(i) => i.id === 'todo-comment' && i.filePath === filePath
|
|
741
|
+
);
|
|
742
|
+
expect(todoIssues.length).toBeLessThanOrEqual(5);
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|