security-asset-correlator 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 (42) hide show
  1. security_asset_correlator-1.0.0/PKG-INFO +443 -0
  2. security_asset_correlator-1.0.0/README.md +409 -0
  3. security_asset_correlator-1.0.0/pyproject.toml +74 -0
  4. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/PKG-INFO +443 -0
  5. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/SOURCES.txt +40 -0
  6. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/dependency_links.txt +1 -0
  7. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/entry_points.txt +2 -0
  8. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/requires.txt +17 -0
  9. security_asset_correlator-1.0.0/security_asset_correlator.egg-info/top_level.txt +1 -0
  10. security_asset_correlator-1.0.0/setup.cfg +4 -0
  11. security_asset_correlator-1.0.0/src/__init__.py +0 -0
  12. security_asset_correlator-1.0.0/src/api/__init__.py +0 -0
  13. security_asset_correlator-1.0.0/src/api/main.py +81 -0
  14. security_asset_correlator-1.0.0/src/api/routes/__init__.py +0 -0
  15. security_asset_correlator-1.0.0/src/api/routes/assets.py +182 -0
  16. security_asset_correlator-1.0.0/src/api/routes/coverage.py +77 -0
  17. security_asset_correlator-1.0.0/src/api/routes/vulnerabilities.py +152 -0
  18. security_asset_correlator-1.0.0/src/config/__init__.py +0 -0
  19. security_asset_correlator-1.0.0/src/config/canonical_mapping.yaml +191 -0
  20. security_asset_correlator-1.0.0/src/config/match_thresholds.yaml +58 -0
  21. security_asset_correlator-1.0.0/src/config/source_confidence.yaml +68 -0
  22. security_asset_correlator-1.0.0/src/config/source_mappings.yaml +170 -0
  23. security_asset_correlator-1.0.0/src/correlator/__init__.py +0 -0
  24. security_asset_correlator-1.0.0/src/correlator/conflict_resolver.py +85 -0
  25. security_asset_correlator-1.0.0/src/correlator/engine.py +322 -0
  26. security_asset_correlator-1.0.0/src/correlator/matcher.py +221 -0
  27. security_asset_correlator-1.0.0/src/correlator/merger.py +170 -0
  28. security_asset_correlator-1.0.0/src/correlator/models.py +108 -0
  29. security_asset_correlator-1.0.0/src/loaders/__init__.py +0 -0
  30. security_asset_correlator-1.0.0/src/loaders/base_loader.py +114 -0
  31. security_asset_correlator-1.0.0/src/loaders/generic_loader.py +388 -0
  32. security_asset_correlator-1.0.0/src/resolvers/__init__.py +0 -0
  33. security_asset_correlator-1.0.0/src/resolvers/hostname_resolver.py +109 -0
  34. security_asset_correlator-1.0.0/src/resolvers/ip_resolver.py +115 -0
  35. security_asset_correlator-1.0.0/src/resolvers/metadata_resolver.py +101 -0
  36. security_asset_correlator-1.0.0/src/store/__init__.py +5 -0
  37. security_asset_correlator-1.0.0/src/store/base.py +61 -0
  38. security_asset_correlator-1.0.0/src/store/memory.py +60 -0
  39. security_asset_correlator-1.0.0/src/store/sql.py +269 -0
  40. security_asset_correlator-1.0.0/tests/test_conflict_resolver.py +186 -0
  41. security_asset_correlator-1.0.0/tests/test_matcher.py +341 -0
  42. security_asset_correlator-1.0.0/tests/test_merger.py +383 -0
@@ -0,0 +1,443 @@
1
+ Metadata-Version: 2.4
2
+ Name: security-asset-correlator
3
+ Version: 1.0.0
4
+ Summary: Cross-tool canonical asset correlation engine for security operations
5
+ Author-email: Apurv Tyagi <tyagiapurv@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/apurvtyagi/security-asset-correlator
8
+ Project-URL: Issues, https://github.com/apurvtyagi/security-asset-correlator/issues
9
+ Project-URL: Documentation, https://github.com/apurvtyagi/security-asset-correlator/tree/main/docs
10
+ Keywords: security,asset,correlation,vulnerability,cmdb,edr,aws
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Information Technology
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Security
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastapi>=0.115.0
20
+ Requires-Dist: uvicorn[standard]>=0.32.0
21
+ Requires-Dist: pydantic>=2.10.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: python-dateutil>=2.9.0
24
+ Requires-Dist: sqlalchemy>=2.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.3; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
28
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
29
+ Requires-Dist: httpx>=0.27; extra == "dev"
30
+ Requires-Dist: ruff>=0.8.0; extra == "dev"
31
+ Requires-Dist: mypy>=1.13; extra == "dev"
32
+ Provides-Extra: postgres
33
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
34
+
35
+ # security-asset-correlator
36
+
37
+ [![CI](https://github.com/apurvtyagi/security-asset-correlator/actions/workflows/ci.yml/badge.svg)](https://github.com/apurvtyagi/security-asset-correlator/actions/workflows/ci.yml)
38
+ [![Version](https://img.shields.io/badge/version-1.0.0-blue)](https://github.com/apurvtyagi/security-asset-correlator/releases)
39
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
41
+
42
+ > Cross-tool canonical asset correlation engine for security operations.
43
+
44
+ Most security programs don't have a vulnerability problem. They have an asset identity problem. The same EC2 instance appears as four distinct records across your cloud inventory, EDR platform, and vulnerability scanners — each with different findings attached. This project is the glue layer that shouldn't have to exist but usually does.
45
+
46
+ ---
47
+
48
+ ## Contents
49
+
50
+ - [The Problem](#the-problem)
51
+ - [What This Does](#what-this-does)
52
+ - [Architecture](#architecture)
53
+ - [Repository Structure](#repository-structure)
54
+ - [Quick Start](#quick-start)
55
+ - [API Endpoints](#api-endpoints)
56
+ - [Testing](#testing)
57
+ - [Adding a New Security Tool](#adding-a-new-security-tool)
58
+ - [Configuration](#configuration)
59
+ - [Design Principles](#design-principles)
60
+ - [Storage Backends](#storage-backends)
61
+ - [Coverage Gap Analysis](#coverage-gap-analysis)
62
+ - [Contributing](#contributing)
63
+ - [License](#license)
64
+
65
+ ---
66
+
67
+ ## The Problem
68
+
69
+ | Tool | How It Knows Your Asset |
70
+ |------|------------------------|
71
+ | AWS / Cloud Provider | `instanceId: i-0a1b2c3d4e5f` |
72
+ | CrowdStrike / EDR | `hostname: prod-api-07.internal` |
73
+ | Tenable / Nessus | `ip: 10.0.4.22` (scan-time) |
74
+ | Qualys | `ip: 10.0.4.23` (different scan window, NATted) |
75
+ | ServiceNow CMDB | `name: PRODAPI007` (manual entry, 8 months stale) |
76
+
77
+ Each tool reports vulnerabilities against its own asset identifier. Without correlation, a single critical CVE appears 3–4 times in your dashboard. Patch coverage looks worse than it is. MTTR metrics are wrong. Prioritization is impossible.
78
+
79
+ ---
80
+
81
+ ## What This Does
82
+
83
+ Builds a **canonical asset graph** — one record per real-world entity — by:
84
+
85
+ 1. Ingesting raw asset records from multiple security tool sources
86
+ 2. Running layered matching logic with explicit confidence scoring
87
+ 3. Merging duplicates into a canonical record with full source lineage
88
+ 4. Mapping vulnerability findings to canonical assets (not tool-specific representations)
89
+ 5. Flagging conflicts with resolution strategy and audit trail
90
+
91
+ ---
92
+
93
+ ## Architecture
94
+
95
+ ```
96
+ ┌─────────────────────────────────────────────────────────┐
97
+ │ SOURCE INGESTION │
98
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
99
+ │ │ AWS API │ │ EDR API │ │ Tenable │ │Qualys │ │
100
+ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └───┬────┘ │
101
+ └───────┼─────────────┼─────────────┼─────────────┼───────┘
102
+ │ │ │ │
103
+ ▼ ▼ ▼ ▼
104
+ ┌─────────────────────────────────────────────────────────┐
105
+ │ NORMALIZATION LAYER │
106
+ │ - Hostname sanitization (.local, -prod, case folding) │
107
+ │ - IP deduplication (public vs private, staleness TTL) │
108
+ │ - Metadata normalization (OS, region, tags) │
109
+ └─────────────────────────┬───────────────────────────────┘
110
+
111
+
112
+ ┌─────────────────────────────────────────────────────────┐
113
+ │ LAYERED MATCHING ENGINE │
114
+ │ │
115
+ │ Layer 1: Hard ID match (instanceId, MAC, agentUUID) │
116
+ │ → confidence: 0.95–1.00, STOP if matched │
117
+ │ │
118
+ │ Layer 2: Hostname match (normalized) │
119
+ │ → confidence: 0.45–0.85 │
120
+ │ │
121
+ │ Layer 3: IP cross-reference (with staleness decay) │
122
+ │ → confidence: 0.60–0.75 │
123
+ │ │
124
+ │ Layers 2+3 combined: hostname×0.60 + ip×0.40 │
125
+ │ │
126
+ │ Layer 4: Metadata correlation (OS + region + account) │
127
+ │ → confidence: up to 0.50 │
128
+ │ │
129
+ │ Threshold: merge if score ≥ 0.70, flag if 0.50–0.69 │
130
+ └─────────────────────────┬───────────────────────────────┘
131
+
132
+
133
+ ┌─────────────────────────────────────────────────────────┐
134
+ │ CANONICAL ASSET STORE │
135
+ │ - One record per physical/virtual entity │
136
+ │ - Source lineage (all contributing records) │
137
+ │ - Conflict log (field disagreements + resolution) │
138
+ │ - Source confidence ranking per field │
139
+ └─────────────────────────┬───────────────────────────────┘
140
+
141
+
142
+ ┌─────────────────────────────────────────────────────────┐
143
+ │ VULNERABILITY DEDUPLICATION │
144
+ │ - CVE findings mapped to canonical_id │
145
+ │ - Deduplication by (canonical_id, cve_id) │
146
+ │ - Unified risk score per asset (not per tool record) │
147
+ └─────────────────────────────────────────────────────────┘
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Repository Structure
153
+
154
+ ```
155
+ security-asset-correlator/
156
+ ├── README.md
157
+ ├── pyproject.toml
158
+ ├── requirements.txt
159
+ ├── docker-compose.yml
160
+ ├── config/
161
+ │ ├── source_mappings.yaml # ★ Field mappings for every source tool (YAML, no Python)
162
+ │ ├── canonical_mapping.yaml # Authority ranks + staleness TTL per field
163
+ │ ├── source_confidence.yaml # Per-source and per-field trust weights
164
+ │ └── match_thresholds.yaml # Merge/flag thresholds + layer score weights
165
+ ├── src/
166
+ │ ├── correlator/
167
+ │ │ ├── models.py # Shared data models (RawAssetRecord, CanonicalAsset, etc.)
168
+ │ │ ├── engine.py # Orchestration + CLI entrypoint
169
+ │ │ ├── matcher.py # 4-layer matching logic
170
+ │ │ ├── merger.py # Canonical record construction
171
+ │ │ └── conflict_resolver.py # Field conflict resolution + audit log
172
+ │ ├── loaders/
173
+ │ │ ├── base_loader.py # BaseLoader ABC + LoaderRegistry (auto-discovers YAML sources)
174
+ │ │ └── generic_loader.py # ★ Config-driven loader engine + transform functions
175
+ │ ├── store/
176
+ │ │ ├── base.py # AssetStore interface
177
+ │ │ ├── memory.py # InMemoryStore with O(1) hard-ID indexes (default)
178
+ │ │ └── sql.py # SQLiteStore + PostgreSQLStore (SQLAlchemy 2.0)
179
+ │ ├── resolvers/
180
+ │ │ ├── hostname_resolver.py # Hostname normalization + generic detection
181
+ │ │ ├── ip_resolver.py # IP staleness decay + multi-NIC handling
182
+ │ │ └── metadata_resolver.py # OS family normalization + tag similarity
183
+ │ └── api/
184
+ │ ├── main.py # FastAPI entrypoint
185
+ │ └── routes/
186
+ │ ├── assets.py # Canonical asset endpoints
187
+ │ ├── vulnerabilities.py # Deduplicated vuln endpoints
188
+ │ └── coverage.py # Coverage gap analysis endpoints
189
+ ├── data/
190
+ │ └── samples/
191
+ │ ├── aws_sample.json
192
+ │ ├── edr_sample.json
193
+ │ ├── tenable_sample.json
194
+ │ └── qualys_sample.json
195
+ ├── tests/
196
+ │ ├── test_matcher.py # 4-layer matching + confidence scoring
197
+ │ ├── test_merger.py # Record merge + vuln deduplication
198
+ │ └── test_conflict_resolver.py # Authority ranking + conflict log
199
+ └── docs/
200
+ ├── design_considerations.md # Architecture rationale + tuning guide
201
+ └── edge_cases.md # 8 documented edge cases with mitigations
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Quick Start
207
+
208
+ **Install from PyPI**
209
+
210
+ ```bash
211
+ pip install security-asset-correlator
212
+ ```
213
+
214
+ **Local (development)**
215
+
216
+ ```bash
217
+ git clone https://github.com/apurvtyagi/security-asset-correlator
218
+ cd security-asset-correlator
219
+ python -m venv .venv && source .venv/bin/activate
220
+ pip install -e ".[dev]"
221
+
222
+ # Run correlation against sample data
223
+ python -m src.correlator.engine --sources data/samples/ --output canonical_assets.json
224
+
225
+ # Start API server
226
+ uvicorn src.api.main:app --reload
227
+ ```
228
+
229
+ **Docker**
230
+
231
+ ```bash
232
+ docker-compose up
233
+ # API available at http://localhost:8000
234
+ # Docs at http://localhost:8000/docs
235
+ ```
236
+
237
+ ---
238
+
239
+ ## API Endpoints
240
+
241
+ | Method | Path | Description |
242
+ |--------|------|-------------|
243
+ | `GET` | `/health` | Liveness check |
244
+ | `GET` | `/api/v1/assets/` | List canonical assets (filter by source, region, type) |
245
+ | `GET` | `/api/v1/assets/{id}` | Single canonical asset |
246
+ | `GET` | `/api/v1/assets/{id}/conflicts` | Field-level conflict audit log |
247
+ | `POST` | `/api/v1/assets/ingest` | Ingest raw records from a named source |
248
+ | `GET` | `/api/v1/assets/review/flagged` | Assets pending human review |
249
+ | `GET` | `/api/v1/vulnerabilities/` | Deduplicated findings (filter by severity, CVE, source) |
250
+ | `GET` | `/api/v1/vulnerabilities/by-asset/{id}` | Findings for one canonical asset |
251
+ | `GET` | `/api/v1/vulnerabilities/summary` | Aggregate stats and top CVEs |
252
+ | `GET` | `/api/v1/coverage/` | Full coverage gap report |
253
+ | `GET` | `/api/v1/coverage/no-edr` | Assets without EDR agent coverage |
254
+ | `GET` | `/api/v1/coverage/no-scanner` | Assets never scanned for vulnerabilities |
255
+ | `GET` | `/api/v1/coverage/shadow-it` | Possible unmanaged / shadow-IT devices |
256
+
257
+ ---
258
+
259
+ ## Testing
260
+
261
+ ```bash
262
+ pytest tests/
263
+ ```
264
+
265
+ 59 tests covering:
266
+ - Hard ID, hostname, IP, and metadata matching paths
267
+ - Confidence score calculation and threshold behaviour
268
+ - Scalar field merge with authority-ranked conflict resolution
269
+ - List union, tag namespacing, and `last_seen` max logic
270
+ - Cross-source CVE deduplication (earliest `first_found`, highest CVSS, source union)
271
+ - Full conflict audit log structure and field attribution
272
+
273
+ ---
274
+
275
+ ## Adding a New Security Tool
276
+
277
+ The loader layer is fully config-driven. Adding support for a new tool — Lacework, Wiz, Microsoft Defender, Prisma Cloud, or anything else — requires only a YAML block and no new Python files.
278
+
279
+ **Step 1 — Add a block to `config/source_mappings.yaml`**
280
+
281
+ ```yaml
282
+ # Microsoft Defender for Endpoint example
283
+ mde:
284
+ source_id_field: id # which field in the API response is the unique ID
285
+ asset_type: workstation
286
+ fields:
287
+ agent_id: id
288
+ hostnames:
289
+ pick: [computerDnsName]
290
+ ip_addresses:
291
+ field: lastIpAddress
292
+ transform: ensure_list # built-in: wraps scalar or list → list
293
+ os_name: osPlatform
294
+ os_version: osVersion
295
+ last_seen:
296
+ field: lastSeen
297
+ transform: iso_datetime # built-in: ISO 8601 string → datetime
298
+ ```
299
+
300
+ That's it for most tools. The engine auto-discovers it:
301
+
302
+ ```python
303
+ from src.loaders.base_loader import LoaderRegistry
304
+
305
+ loader = LoaderRegistry.get("mde") # works immediately
306
+ records = loader.load(raw_api_response) # returns [RawAssetRecord, ...]
307
+ ```
308
+
309
+ **Step 2 (only if the tool has an unusual data shape) — add a `@transform` function**
310
+
311
+ For example, if your tool returns tags as `[{"k": "env", "v": "prod"}]` instead of the common formats:
312
+
313
+ ```python
314
+ # src/loaders/generic_loader.py — add alongside the other transforms
315
+ @transform("mytool_tags")
316
+ def _mytool_tags(value: Any) -> dict:
317
+ if not isinstance(value, list):
318
+ return {}
319
+ return {entry["k"]: entry["v"] for entry in value if "k" in entry}
320
+ ```
321
+
322
+ Then reference it by name in the YAML:
323
+
324
+ ```yaml
325
+ mytool:
326
+ fields:
327
+ tags:
328
+ field: asset_tags
329
+ transform: mytool_tags
330
+ ```
331
+
332
+ **Built-in transforms** (no custom code needed for these common patterns):
333
+
334
+ | Transform | Input | Output |
335
+ |---|---|---|
336
+ | `iso_datetime` | ISO 8601 string | `datetime` |
337
+ | `ensure_list` | scalar or list | `list` |
338
+ | `first_of_list` | list | first non-empty element |
339
+ | `dedup_list` | list | deduplicated list |
340
+ | `aws_platform_to_os` | `"windows"` / `""` | `"Windows"` / `"Linux"` |
341
+ | `az_to_region` | `"us-east-1a"` | `"us-east-1"` |
342
+ | `aws_tags_list` | `[{Key, Value}]` | `{key: value}` |
343
+ | `mac_to_list` | single MAC string | `["aa:bb:cc:dd:ee:ff"]` |
344
+ | `edr_tags` | list of strings or dict | `{str: True}` or passthrough |
345
+ | `tenable_tags` | `[{category, value}]` | `{category: value}` |
346
+ | `tenable_vulns` | Tenable findings array | `[VulnerabilityFinding]` |
347
+ | `qualys_vulns` | Qualys `DETECTIONS` dict | `[VulnerabilityFinding]` |
348
+
349
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for a full walkthrough.
350
+
351
+ ---
352
+
353
+ ## Configuration
354
+
355
+ All thresholds and authority rankings are in `config/` — no hardcoded values in the matching or merge logic.
356
+
357
+ | File | Controls |
358
+ |------|----------|
359
+ | `source_mappings.yaml` | Field mappings for every source tool — edit to add new tools |
360
+ | `canonical_mapping.yaml` | Authority ranks per field per source, staleness TTL |
361
+ | `source_confidence.yaml` | Per-source and per-field trust weights |
362
+ | `match_thresholds.yaml` | Merge/flag thresholds, layer scores, combination weights |
363
+
364
+ To adjust who wins a hostname conflict between EDR and AWS, change `canonical_fields.hostname.authority_rank` in `canonical_mapping.yaml`. To make IP matching more conservative in a heavy-NAT environment, lower `layer_scores.ip.private_ip_match` in `match_thresholds.yaml`.
365
+
366
+ ---
367
+
368
+ ## Design Principles
369
+
370
+ - **Prefer explicit over inferred** — hard identifiers always win over heuristic matches
371
+ - **Source authority is per-field, not per-source** — AWS is authoritative for `region` but not `hostname`; EDR is authoritative for `hostname` but not `instanceId`
372
+ - **Conflicts are data** — every field disagreement is logged with both values, both sources, both authority ranks, and the resolution taken
373
+ - **Merge threshold is tunable** — default 0.70 works for most environments; high-churn ephemeral infra may need adjustment
374
+ - **No silent drops** — unmatched records are surfaced, not discarded; ambiguous matches are flagged for human review
375
+
376
+ ---
377
+
378
+ ## Storage Backends
379
+
380
+ The engine uses a pluggable `AssetStore` interface with two built-in backends:
381
+
382
+ | Backend | When to use |
383
+ |---------|-------------|
384
+ | `InMemoryStore` (default) | Single-process, ephemeral, tests |
385
+ | `SQLiteStore` | Single-server persistent deployments |
386
+ | `PostgreSQLStore` | Multi-instance / high-availability |
387
+
388
+ ```python
389
+ from src.correlator.engine import CorrelationEngine
390
+ from src.store.sql import SQLiteStore
391
+
392
+ store = SQLiteStore("sqlite:///assets.db")
393
+ store.init_schema()
394
+ engine = CorrelationEngine(store=store)
395
+ ```
396
+
397
+ For PostgreSQL, install the extra and pass a `postgresql://` URL:
398
+
399
+ ```bash
400
+ pip install security-asset-correlator[postgres]
401
+ ```
402
+
403
+ ```python
404
+ from src.store.sql import PostgreSQLStore
405
+ store = PostgreSQLStore("postgresql://user:pass@host/dbname")
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Coverage Gap Analysis
411
+
412
+ After ingesting records, call the coverage report to find blind spots:
413
+
414
+ ```bash
415
+ GET /api/v1/coverage/
416
+ ```
417
+
418
+ ```json
419
+ {
420
+ "total_assets": 142,
421
+ "no_edr": { "count": 18, "canonical_ids": ["..."] },
422
+ "no_scanner": { "count": 9, "canonical_ids": ["..."] },
423
+ "shadow_it": { "count": 3, "canonical_ids": ["..."] }
424
+ }
425
+ ```
426
+
427
+ - **no_edr** — assets not enrolled in any endpoint agent (CrowdStrike, SentinelOne)
428
+ - **no_scanner** — assets that have never been scanned by Tenable or Qualys
429
+ - **shadow_it** — assets seen only by scanners with no cloud inventory or EDR record — possible unmanaged devices
430
+
431
+ ---
432
+
433
+ ## Contributing
434
+
435
+ PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide, including how to add a new source loader in ~15 minutes.
436
+
437
+ Issues for new source loaders, edge case coverage, and confidence model improvements especially appreciated.
438
+
439
+ ---
440
+
441
+ ## License
442
+
443
+ MIT