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.
- security_asset_correlator-1.0.0/PKG-INFO +443 -0
- security_asset_correlator-1.0.0/README.md +409 -0
- security_asset_correlator-1.0.0/pyproject.toml +74 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/PKG-INFO +443 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/SOURCES.txt +40 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/dependency_links.txt +1 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/entry_points.txt +2 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/requires.txt +17 -0
- security_asset_correlator-1.0.0/security_asset_correlator.egg-info/top_level.txt +1 -0
- security_asset_correlator-1.0.0/setup.cfg +4 -0
- security_asset_correlator-1.0.0/src/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/api/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/api/main.py +81 -0
- security_asset_correlator-1.0.0/src/api/routes/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/api/routes/assets.py +182 -0
- security_asset_correlator-1.0.0/src/api/routes/coverage.py +77 -0
- security_asset_correlator-1.0.0/src/api/routes/vulnerabilities.py +152 -0
- security_asset_correlator-1.0.0/src/config/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/config/canonical_mapping.yaml +191 -0
- security_asset_correlator-1.0.0/src/config/match_thresholds.yaml +58 -0
- security_asset_correlator-1.0.0/src/config/source_confidence.yaml +68 -0
- security_asset_correlator-1.0.0/src/config/source_mappings.yaml +170 -0
- security_asset_correlator-1.0.0/src/correlator/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/correlator/conflict_resolver.py +85 -0
- security_asset_correlator-1.0.0/src/correlator/engine.py +322 -0
- security_asset_correlator-1.0.0/src/correlator/matcher.py +221 -0
- security_asset_correlator-1.0.0/src/correlator/merger.py +170 -0
- security_asset_correlator-1.0.0/src/correlator/models.py +108 -0
- security_asset_correlator-1.0.0/src/loaders/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/loaders/base_loader.py +114 -0
- security_asset_correlator-1.0.0/src/loaders/generic_loader.py +388 -0
- security_asset_correlator-1.0.0/src/resolvers/__init__.py +0 -0
- security_asset_correlator-1.0.0/src/resolvers/hostname_resolver.py +109 -0
- security_asset_correlator-1.0.0/src/resolvers/ip_resolver.py +115 -0
- security_asset_correlator-1.0.0/src/resolvers/metadata_resolver.py +101 -0
- security_asset_correlator-1.0.0/src/store/__init__.py +5 -0
- security_asset_correlator-1.0.0/src/store/base.py +61 -0
- security_asset_correlator-1.0.0/src/store/memory.py +60 -0
- security_asset_correlator-1.0.0/src/store/sql.py +269 -0
- security_asset_correlator-1.0.0/tests/test_conflict_resolver.py +186 -0
- security_asset_correlator-1.0.0/tests/test_matcher.py +341 -0
- 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
|
+
[](https://github.com/apurvtyagi/security-asset-correlator/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/apurvtyagi/security-asset-correlator/releases)
|
|
39
|
+
[](https://www.python.org/downloads/)
|
|
40
|
+
[](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
|