appsec-scan-router 1.0.0__tar.gz

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.
Files changed (30) hide show
  1. appsec_scan_router-1.0.0/LICENSE +21 -0
  2. appsec_scan_router-1.0.0/PKG-INFO +478 -0
  3. appsec_scan_router-1.0.0/README.md +449 -0
  4. appsec_scan_router-1.0.0/ado_mobile_scanner.py +5 -0
  5. appsec_scan_router-1.0.0/appsec_scan_router/__init__.py +269 -0
  6. appsec_scan_router-1.0.0/appsec_scan_router/__main__.py +5 -0
  7. appsec_scan_router-1.0.0/appsec_scan_router/activity.py +63 -0
  8. appsec_scan_router-1.0.0/appsec_scan_router/azure.py +307 -0
  9. appsec_scan_router-1.0.0/appsec_scan_router/cli.py +182 -0
  10. appsec_scan_router-1.0.0/appsec_scan_router/constants.py +123 -0
  11. appsec_scan_router-1.0.0/appsec_scan_router/detection.py +289 -0
  12. appsec_scan_router-1.0.0/appsec_scan_router/metadata.py +456 -0
  13. appsec_scan_router-1.0.0/appsec_scan_router/models.py +94 -0
  14. appsec_scan_router-1.0.0/appsec_scan_router/reports.py +166 -0
  15. appsec_scan_router-1.0.0/appsec_scan_router/scanner.py +794 -0
  16. appsec_scan_router-1.0.0/appsec_scan_router/sdk.py +19 -0
  17. appsec_scan_router-1.0.0/appsec_scan_router/store_lookup.py +384 -0
  18. appsec_scan_router-1.0.0/appsec_scan_router/utils.py +113 -0
  19. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/PKG-INFO +478 -0
  20. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/SOURCES.txt +28 -0
  21. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/dependency_links.txt +1 -0
  22. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/entry_points.txt +4 -0
  23. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/requires.txt +2 -0
  24. appsec_scan_router-1.0.0/appsec_scan_router.egg-info/top_level.txt +4 -0
  25. appsec_scan_router-1.0.0/mobile_app_inventory_tracer.py +5 -0
  26. appsec_scan_router-1.0.0/mobile_scanner/__init__.py +1 -0
  27. appsec_scan_router-1.0.0/mobile_scanner/__main__.py +5 -0
  28. appsec_scan_router-1.0.0/pyproject.toml +53 -0
  29. appsec_scan_router-1.0.0/setup.cfg +4 -0
  30. appsec_scan_router-1.0.0/tests/test_ado_mobile_scanner.py +751 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AppSec Scan Router contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,478 @@
1
+ Metadata-Version: 2.4
2
+ Name: appsec-scan-router
3
+ Version: 1.0.0
4
+ Summary: SDK and CLI for routing Azure DevOps mobile app inventory scans with optional public store validation
5
+ Author: AppSec Scan Router contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/h0p3sf4ll/mobile-app-inventory-tracer
8
+ Project-URL: Repository, https://github.com/h0p3sf4ll/mobile-app-inventory-tracer
9
+ Project-URL: Issues, https://github.com/h0p3sf4ll/mobile-app-inventory-tracer/issues
10
+ Keywords: appsec,sdk,azure-devops,mobile,inventory,android,ios,app-store,google-play
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Version Control :: Git
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: openpyxl>=3.1.0
27
+ Requires-Dist: requests>=2.31.0
28
+ Dynamic: license-file
29
+
30
+ # AppSec Scan Router
31
+
32
+ AppSec Scan Router is a Python SDK, CLI, and Dockerized scanner for Azure DevOps mobile application inventory. It identifies mobile apps across large Git estates, extracts app metadata, captures contributor and activity signals, validates public app store listings when requested, and writes Excel-ready reports as the scan runs.
33
+
34
+ It is designed for engineering, platform, security, and enterprise architecture teams that need a reliable inventory of mobile codebases without cloning every repository or depending on broad keyword search.
35
+
36
+ ## Highlights
37
+
38
+ - Scans each repository's configured default branch, with a controlled deploy-branch fallback when none is set
39
+ - Detects Android, iOS, Flutter, React Native, Expo, Ionic, Capacitor, Cordova, Xamarin, and .NET MAUI signals
40
+ - Parses structured manifests and project files instead of relying on naive keyword matching
41
+ - Extracts app name, version, bundle/package identifier, source of identifier, contributors, and latest branch activity
42
+ - Splits Excel output into active and older branch worksheets, defaulting to `Active 90d` and `Older 90d`
43
+ - Streams CSV and JSON rows while the scan is running
44
+ - Optionally enriches detected identifiers with public Apple App Store and Google Play metadata
45
+ - Runs as a Python package, CLI, importable library, or Docker container
46
+ - Keeps Azure DevOps access read-only and fetches only allow-listed metadata/configuration files
47
+
48
+ ## How It Works
49
+
50
+ The scanner uses the Azure DevOps REST API to list projects, repositories, default-branch tree items, selected file contents, and commit history. For each repository, it scans the branch in Azure DevOps `defaultBranch`, such as `main`, `master`, `develop`, or another configured default.
51
+
52
+ If Azure DevOps does not report a default branch for a repository, the scanner resolves one fallback branch instead of scanning every branch. It first checks Azure DevOps build definitions for repository-linked branch settings and branch filters. If no pipeline-associated branch can be resolved, it selects the strongest deployment-like branch name from the repo refs, prioritizing names such as `production`, `prod`, `preprod`, `release`, `main`, `master`, `development`, `develop`, and `dev`.
53
+
54
+ Azure DevOps does not provide a single universal "production branch" field across all repos and deployment models. Pipeline fallback is therefore best-effort and depends on build definitions being available to the PAT. Release pipelines, external deployment systems, and manually deployed branches may not be visible through the read-only Code APIs.
55
+
56
+ It fetches only files that can provide strong mobile signals or metadata, such as:
57
+
58
+ - `AndroidManifest.xml`
59
+ - `Info.plist`
60
+ - `InfoPlist.strings`
61
+ - `project.pbxproj`
62
+ - `.xcconfig`
63
+ - `build.gradle`
64
+ - `build.gradle.kts`
65
+ - `gradle.properties`
66
+ - `package.json`
67
+ - `app.json`
68
+ - `expo.json`
69
+ - `pubspec.yaml`
70
+ - `.csproj`
71
+ - `.props`
72
+ - `capacitor.config.*`
73
+ - `ionic.config.json`
74
+ - `config.xml`
75
+ - Azure pipeline YAML files
76
+
77
+ Detection is evidence-based. A repository branch is included when structured signals meet the configured confidence threshold. Generic `.csproj` files, generic `config.xml` files, and weak pipeline-only clues are not enough on their own to classify a repository as an app.
78
+
79
+ ## What It Detects
80
+
81
+ | Category | Strong signals |
82
+ | --- | --- |
83
+ | Android | Android manifest, Gradle Android application plugin, `applicationId`, `namespace`, `versionName` |
84
+ | iOS | `Info.plist`, `InfoPlist.strings`, Xcode build settings, bundle identifiers, marketing version |
85
+ | Flutter | `pubspec.yaml` Flutter SDK dependency with native project layout |
86
+ | React Native / Expo | `package.json`, Expo config, native Android/iOS project layout |
87
+ | Ionic / Capacitor / Cordova | Capacitor config, Cordova widget config, Ionic config, package dependencies |
88
+ | Xamarin / MAUI | `.csproj`, `UseMaui`, mobile target frameworks, `ApplicationId` |
89
+ | Mobile pipelines | Mobile build/task evidence as supporting context |
90
+
91
+ ## App Metadata Extraction
92
+
93
+ When possible, the scanner extracts:
94
+
95
+ - `mobile_name`, such as `Agsnap`
96
+ - `mobile_version`, such as `1.0.2`
97
+ - `mobile_identifier`, such as `com.pepsico.agsnap`
98
+ - `mobile_identifier_source`, such as `Info.plist`, `Gradle applicationId/namespace`, or `Xcode build settings`
99
+ - `mobile_identifier_status`, either `found` or `missing_from_scanned_files`
100
+
101
+ It resolves common indirection patterns before deciding that an identifier is missing:
102
+
103
+ - Gradle property placeholders such as `${appId}`
104
+ - Xcode build setting placeholders such as `$(PRODUCT_BUNDLE_IDENTIFIER)`
105
+ - MSBuild `.props` values
106
+ - iOS plist references
107
+ - Android string resources for display names
108
+
109
+ The scanner does not invent identifiers. If a repo generates identifiers from CI/CD variables, private variable groups, secrets, flavors, external files, or runtime build logic that is not present in Azure DevOps, the identifier is reported as missing.
110
+
111
+ ## Store Enrichment
112
+
113
+ Store lookup is optional and disabled by default. Enable it with `--store-lookup`.
114
+
115
+ When enabled:
116
+
117
+ - Apple App Store lookup uses the detected bundle identifier against Apple public lookup data
118
+ - Google Play lookup checks the public app details page by package identifier
119
+ - Results are cached by identifier and platform during the scan
120
+ - Lookup runs only after a resolved branch has already been classified as mobile
121
+
122
+ Google Play public lookup can confirm public listings, but it cannot see private/internal apps or Play Console-only listings. Authenticated Google Play Developer API support can be layered in separately for organizations that own the apps and can provide Android Publisher OAuth credentials.
123
+
124
+ ## Installation
125
+
126
+ ### Requirements
127
+
128
+ - Python 3.10 or newer
129
+ - Azure DevOps PAT with read access to Projects and Code
130
+ - Optional Azure DevOps Build read access for pipeline-associated fallback branches when a repo has no default branch
131
+ - Network access to `dev.azure.com`
132
+ - Optional network access to Apple and Google Play endpoints when `--store-lookup` is enabled
133
+
134
+ ### PyPI Install
135
+
136
+ ```bash
137
+ python -m pip install appsec-scan-router
138
+ ```
139
+
140
+ ### Local Development Setup
141
+
142
+ ```bash
143
+ git clone https://github.com/h0p3sf4ll/mobile-app-inventory-tracer.git
144
+ cd mobile-app-inventory-tracer
145
+ python3 -m venv .venv
146
+ source .venv/bin/activate
147
+ python -m pip install -r requirements.txt
148
+ python -m pip install -e .
149
+ ```
150
+
151
+ Set your Azure DevOps token as an environment variable:
152
+
153
+ ```bash
154
+ export ADO_PAT="your-token-here"
155
+ ```
156
+
157
+ ## Quick Start
158
+
159
+ Scan every project in an Azure DevOps organization:
160
+
161
+ ```bash
162
+ appsec-scan-router --org PepsiCoIT --out-dir reports
163
+ ```
164
+
165
+ Scan one project:
166
+
167
+ ```bash
168
+ appsec-scan-router --org PepsiCoIT --project "Go_To_Market" --out-dir reports
169
+ ```
170
+
171
+ Only include medium and high confidence matches:
172
+
173
+ ```bash
174
+ appsec-scan-router --org PepsiCoIT --out-dir reports --min-confidence medium
175
+ ```
176
+
177
+ Fast profile for very large organizations:
178
+
179
+ ```bash
180
+ appsec-scan-router \
181
+ --org PepsiCoIT \
182
+ --out-dir reports \
183
+ --min-confidence medium \
184
+ --max-workers 12 \
185
+ --branch-workers 32 \
186
+ --content-workers 32 \
187
+ --activity-mode latest
188
+ ```
189
+
190
+ Enable public store enrichment:
191
+
192
+ ```bash
193
+ appsec-scan-router --org PepsiCoIT --out-dir reports --store-lookup --store-country US
194
+ ```
195
+
196
+ Legacy commands remain available for compatibility:
197
+
198
+ ```bash
199
+ mobile-app-inventory-tracer --org PepsiCoIT --out-dir reports
200
+ ado-mobile-scanner --org PepsiCoIT --out-dir reports
201
+ ```
202
+
203
+ You can also run from source:
204
+
205
+ ```bash
206
+ python -m appsec_scan_router --org PepsiCoIT --out-dir reports
207
+ ```
208
+
209
+ ## Docker
210
+
211
+ Build the image:
212
+
213
+ ```bash
214
+ docker build -t appsec-scan-router .
215
+ ```
216
+
217
+ Run a scan and write reports to a local directory:
218
+
219
+ ```bash
220
+ mkdir -p reports
221
+ docker run --rm \
222
+ -e ADO_PAT="$ADO_PAT" \
223
+ -v "$PWD/reports:/reports" \
224
+ appsec-scan-router \
225
+ --org PepsiCoIT \
226
+ --out-dir /reports \
227
+ --min-confidence medium
228
+ ```
229
+
230
+ Run with public store enrichment:
231
+
232
+ ```bash
233
+ docker run --rm \
234
+ -e ADO_PAT="$ADO_PAT" \
235
+ -v "$PWD/reports:/reports" \
236
+ appsec-scan-router \
237
+ --org PepsiCoIT \
238
+ --out-dir /reports \
239
+ --store-lookup \
240
+ --store-country US
241
+ ```
242
+
243
+ The container runs as a non-root `scanner` user and writes to `/reports`.
244
+
245
+ ## CLI Reference
246
+
247
+ | Option | Required | Default | Description |
248
+ | --- | --- | --- | --- |
249
+ | `--org` | Yes | | Azure DevOps organization name |
250
+ | `--project` | No | all projects | Project name to scan |
251
+ | `--pat` | No | `ADO_PAT` | Azure DevOps PAT; prefer the environment variable |
252
+ | `--out-dir` | No | current directory | Output directory |
253
+ | `--out-prefix` | No | `appsec_scan_router` | Output filename prefix |
254
+ | `--max-workers` | No | `8` | Concurrent repository preparation tasks |
255
+ | `--branch-workers` | No | `16` | Concurrent resolved-branch scans |
256
+ | `--content-workers` | No | `16` | Concurrent selected-file fetches |
257
+ | `--max-commits-per-repo` | No | `0` | Commit history limit per matched branch; `0` means all available history |
258
+ | `--timeout` | No | `30` | Azure DevOps HTTP timeout in seconds |
259
+ | `--min-confidence` | No | `low` | Minimum detection confidence: `low`, `medium`, or `high` |
260
+ | `--branch-age-days` | No | `90` | Active/older worksheet cutoff |
261
+ | `--activity-mode` | No | `contributors` | `contributors` walks configured commit history; `latest` only fetches the latest commit |
262
+ | `--store-lookup` | No | disabled | Enable public app store enrichment |
263
+ | `--store-country` | No | `US` | Two-letter public store country code |
264
+ | `--store-timeout` | No | `15` | Store lookup HTTP timeout in seconds |
265
+ | `--verbose` | No | disabled | Enable debug logging |
266
+
267
+ ## Outputs
268
+
269
+ The scanner creates output files as soon as the run starts and appends matching rows as resolved branches are detected:
270
+
271
+ - `appsec_scan_router.csv`
272
+ - `appsec_scan_router.json`
273
+ - `appsec_scan_router.xlsx`
274
+
275
+ The Excel workbook includes:
276
+
277
+ - `Active 90d`: matched app branches changed within the active window
278
+ - `Older 90d`: matched app branches with no changes inside the active window
279
+
280
+ If you change `--branch-age-days`, worksheet names change accordingly, such as `Active 60d` and `Older 60d`.
281
+
282
+ ## Output Schema
283
+
284
+ Core inventory fields:
285
+
286
+ | Field | Description |
287
+ | --- | --- |
288
+ | `project` | Azure DevOps project name |
289
+ | `repo_name` | Repository name |
290
+ | `branch_name` | Resolved repository branch where the app was detected |
291
+ | `branch_last_updated` | Latest branch commit timestamp seen by the scanner |
292
+ | `branch_age_bucket` | Active/older age bucket |
293
+ | `web_url` | Azure DevOps repository URL |
294
+ | `mobile_name` | Best-effort app display name |
295
+ | `mobile_version` | Best-effort app version |
296
+ | `mobile_identifier` | Best-effort bundle/package identifier |
297
+ | `mobile_identifier_source` | Source family where the identifier was found |
298
+ | `mobile_identifier_status` | `found` or `missing_from_scanned_files` |
299
+ | `contributing_developers` | Semicolon-separated unique commit authors |
300
+ | `last_updated` | Same value as `branch_last_updated`, retained for compatibility |
301
+ | `confidence` | Detection confidence |
302
+ | `score` | Weighted evidence score |
303
+ | `categories` | Semicolon-separated matched category names |
304
+ | `category_*` | Excel-filter-friendly `TRUE` / `FALSE` columns |
305
+ | `detection_evidence` | JSON evidence details used for classification |
306
+
307
+ Store enrichment fields:
308
+
309
+ | Field | Description |
310
+ | --- | --- |
311
+ | `store_lookup_status` | Aggregate store lookup status |
312
+ | `store_validation_passed` | `TRUE` when all requested store validations found a public listing |
313
+ | `store_platforms` | Stores where a public listing was found |
314
+ | `apple_app_store_name` | Public Apple App Store app name |
315
+ | `apple_app_store_identifier` | Bundle identifier returned by Apple |
316
+ | `apple_app_store_url` | Public Apple App Store URL |
317
+ | `apple_app_store_version` | Public Apple App Store version |
318
+ | `apple_app_store_last_updated` | Public Apple App Store release/update timestamp |
319
+ | `apple_app_store_validation_passed` | `TRUE` when Apple lookup found a public listing |
320
+ | `apple_app_store_lookup_status` | Apple lookup status |
321
+ | `google_play_name` | Public Google Play app name |
322
+ | `google_play_identifier` | Google Play package identifier checked |
323
+ | `google_play_url` | Public Google Play URL |
324
+ | `google_play_version` | Best-effort version from public page metadata |
325
+ | `google_play_last_updated` | Best-effort update date from public page metadata |
326
+ | `google_play_validation_passed` | `TRUE` when Google Play lookup found a public listing |
327
+ | `google_play_lookup_status` | Google Play public-page lookup status |
328
+
329
+ Validation fields are always `TRUE` or `FALSE`. They are `FALSE` when lookup is disabled, the identifier is missing, the public listing is not found, or the lookup returns an error.
330
+
331
+ ## SDK Usage
332
+
333
+ The scanner can be embedded in another Python application through the `appsec_scan_router` SDK.
334
+
335
+ ```python
336
+ from pathlib import Path
337
+
338
+ from appsec_scan_router import ScanConfig, scan_to_reports
339
+
340
+ config = ScanConfig(
341
+ org="PepsiCoIT",
342
+ pat="your-token-here",
343
+ project=None,
344
+ out_dir=Path("reports"),
345
+ out_prefix="appsec_scan_router",
346
+ max_workers=8,
347
+ branch_workers=16,
348
+ content_workers=16,
349
+ max_commits_per_repo=2000,
350
+ timeout_seconds=30,
351
+ min_confidence="medium",
352
+ branch_age_days=90,
353
+ activity_mode="contributors",
354
+ store_lookup=True,
355
+ store_country="US",
356
+ store_timeout_seconds=15,
357
+ )
358
+
359
+ results, csv_path, json_path, xlsx_path = scan_to_reports(config)
360
+ ```
361
+
362
+ Use the SDK facade when an object-oriented boundary is cleaner for your application:
363
+
364
+ ```python
365
+ from appsec_scan_router import AppSecScanRouter
366
+
367
+ router = AppSecScanRouter(config)
368
+ results, csv_path, json_path, xlsx_path = router.scan_to_reports()
369
+ ```
370
+
371
+ Use `scan(config)` to receive rows without writing reports:
372
+
373
+ ```python
374
+ from appsec_scan_router import scan
375
+
376
+ rows = scan(config)
377
+ ```
378
+
379
+ Stream rows into another process:
380
+
381
+ ```python
382
+ from appsec_scan_router import scan
383
+
384
+ def handle_row(row):
385
+ print(row["project"], row["repo_name"], row["branch_name"], row["mobile_identifier"])
386
+
387
+ rows = scan(config, on_result=handle_row)
388
+ ```
389
+
390
+ The legacy `mobile_scanner` and `ado_mobile_scanner` imports remain available as compatibility aliases, but new integrations should import `appsec_scan_router`.
391
+
392
+ ## Project Layout
393
+
394
+ ```text
395
+ appsec_scan_router/
396
+ activity.py Commit authors and last-updated extraction
397
+ azure.py Azure DevOps REST client
398
+ cli.py CLI argument parsing
399
+ constants.py Shared constants and report schema
400
+ detection.py Evidence-based mobile branch classification
401
+ metadata.py App metadata extraction
402
+ models.py Dataclasses and errors
403
+ reports.py CSV, JSON, and Excel writers
404
+ scanner.py Scan orchestration
405
+ store_lookup.py Optional public app store enrichment
406
+ utils.py Parsing and cleanup helpers
407
+ mobile_scanner/ Compatibility import package
408
+ ado_mobile_scanner.py Compatibility wrapper
409
+ mobile_app_inventory_tracer.py Compatibility wrapper
410
+ tests/ Unit tests
411
+ Dockerfile Container definition
412
+ ```
413
+
414
+ ## Accuracy Notes
415
+
416
+ `mobile_identifier` can be empty when an app identifier is generated outside the files available to the scanner. Common causes include CI/CD variables, private variable groups, build flavors, environment-specific files, secrets, or app catalog packaging steps.
417
+
418
+ `mobile_name` can be empty when the display name is localized, generated, or declared only in native project files that are not present in the scanned branch.
419
+
420
+ `mobile_version` can be empty when versioning is generated by pipeline tasks, Gradle logic, Xcode build settings, or environment-specific files.
421
+
422
+ Placeholder versions such as `999.999.999` are treated as sentinel values and suppressed so they are not mistaken for release versions.
423
+
424
+ Store metadata is not the same as source metadata. App Store and Google Play values reflect public listing data where available; branch timestamps reflect repository activity.
425
+
426
+ ## Performance Guidance
427
+
428
+ Start with:
429
+
430
+ ```bash
431
+ appsec-scan-router --org PepsiCoIT --out-dir reports --max-workers 8 --content-workers 16 --min-confidence medium
432
+ ```
433
+
434
+ For very large organizations, the scanner uses three independent pools:
435
+
436
+ - `--max-workers` prepares repositories and resolves default or fallback branches
437
+ - `--branch-workers` scans resolved branches after they are prepared
438
+ - `--content-workers` fetches selected manifest and configuration files
439
+
440
+ Increase concurrency only if Azure DevOps responds quickly and throttling is not observed. Reduce it if you see `429`, timeout, or transient service errors.
441
+
442
+ Contributor extraction happens only after a resolved branch passes detection. Use `--max-commits-per-repo` if full commit history is too expensive for large estates.
443
+
444
+ Use `--activity-mode latest` for the fastest large-org inventory pass. It captures `last_updated` from the latest branch commit and leaves `contributing_developers` empty, avoiding full commit-history walks. Use `--activity-mode contributors` when the complete contributor column matters more than speed.
445
+
446
+ Store lookup is also performed only after detection. Leave `--store-lookup` off for the fastest inventory scan.
447
+
448
+ ## Security
449
+
450
+ - Use a read-only Azure DevOps PAT scoped to the smallest practical set of projects and repositories
451
+ - Prefer `ADO_PAT` over `--pat` so tokens are not stored in shell history
452
+ - Do not commit generated reports if they may contain internal repository names or contributor emails
453
+ - The scanner does not clone repositories
454
+ - The scanner fetches only allow-listed source/configuration files needed for detection
455
+ - Docker runs as a non-root user
456
+
457
+ ## Testing
458
+
459
+ ```bash
460
+ python -m unittest discover -s tests
461
+ python -m compileall ado_mobile_scanner.py mobile_app_inventory_tracer.py appsec_scan_router mobile_scanner tests
462
+ ```
463
+
464
+ ## Contributing
465
+
466
+ Issues and pull requests are welcome. Useful contributions include:
467
+
468
+ - Additional structured metadata parsers
469
+ - Better evidence rules for mobile frameworks
470
+ - Authenticated Google Play Developer API enrichment
471
+ - Additional report formats
472
+ - Performance improvements for very large Azure DevOps organizations
473
+
474
+ Please include tests for parser, detection, and reporting changes.
475
+
476
+ ## License
477
+
478
+ AppSec Scan Router is released under the MIT License. See [LICENSE](LICENSE).