iridium-core 0.1.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.
- iridium_core-0.1.0/.gitignore +33 -0
- iridium_core-0.1.0/PKG-INFO +89 -0
- iridium_core-0.1.0/README.md +66 -0
- iridium_core-0.1.0/pyproject.toml +36 -0
- iridium_core-0.1.0/src/iridium_core/__init__.py +6 -0
- iridium_core-0.1.0/src/iridium_core/cache/__init__.py +5 -0
- iridium_core-0.1.0/src/iridium_core/cache/ast_cache.py +109 -0
- iridium_core-0.1.0/src/iridium_core/deps/__init__.py +5 -0
- iridium_core-0.1.0/src/iridium_core/deps/lockfiles.py +100 -0
- iridium_core-0.1.0/src/iridium_core/extract/__init__.py +17 -0
- iridium_core-0.1.0/src/iridium_core/extract/base.py +29 -0
- iridium_core-0.1.0/src/iridium_core/extract/javascript_extractor.py +199 -0
- iridium_core-0.1.0/src/iridium_core/extract/python_extractor.py +285 -0
- iridium_core-0.1.0/src/iridium_core/git/__init__.py +15 -0
- iridium_core-0.1.0/src/iridium_core/git/tree_hash.py +58 -0
- iridium_core-0.1.0/src/iridium_core/graph/__init__.py +12 -0
- iridium_core-0.1.0/src/iridium_core/graph/bfs.py +67 -0
- iridium_core-0.1.0/src/iridium_core/graph/scc.py +78 -0
- iridium_core-0.1.0/src/iridium_core/ignore/__init__.py +5 -0
- iridium_core-0.1.0/src/iridium_core/ignore/matcher.py +44 -0
- iridium_core-0.1.0/src/iridium_core/models/__init__.py +15 -0
- iridium_core-0.1.0/src/iridium_core/models/enums.py +22 -0
- iridium_core-0.1.0/src/iridium_core/models/fragment.py +35 -0
- iridium_core-0.1.0/src/iridium_core/models/payload.py +109 -0
- iridium_core-0.1.0/src/iridium_core/sandbox/__init__.py +17 -0
- iridium_core-0.1.0/src/iridium_core/sandbox/pool.py +77 -0
- iridium_core-0.1.0/src/iridium_core/sanitize/__init__.py +5 -0
- iridium_core-0.1.0/src/iridium_core/sanitize/scrubber.py +44 -0
- iridium_core-0.1.0/src/iridium_core/workspace/__init__.py +5 -0
- iridium_core-0.1.0/src/iridium_core/workspace/indexer.py +177 -0
- iridium_core-0.1.0/tests/test_backend_payload_compat.py +106 -0
- iridium_core-0.1.0/tests/test_cache.py +36 -0
- iridium_core-0.1.0/tests/test_deps.py +46 -0
- iridium_core-0.1.0/tests/test_extract_javascript.py +60 -0
- iridium_core-0.1.0/tests/test_extract_python.py +67 -0
- iridium_core-0.1.0/tests/test_git.py +20 -0
- iridium_core-0.1.0/tests/test_graph.py +40 -0
- iridium_core-0.1.0/tests/test_graph_extended.py +18 -0
- iridium_core-0.1.0/tests/test_ignore.py +23 -0
- iridium_core-0.1.0/tests/test_indexer.py +40 -0
- iridium_core-0.1.0/tests/test_payload.py +105 -0
- iridium_core-0.1.0/tests/test_sandbox.py +30 -0
- iridium_core-0.1.0/tests/test_sanitize.py +23 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Byte-compiled / cache
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Iridium local state
|
|
14
|
+
.iridium/
|
|
15
|
+
|
|
16
|
+
# Test / tooling
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
uv.lock
|
|
22
|
+
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
|
|
28
|
+
# OS
|
|
29
|
+
.DS_Store
|
|
30
|
+
|
|
31
|
+
# Env secrets
|
|
32
|
+
.env
|
|
33
|
+
!.env.example
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: iridium-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local AST extraction, graph assembly, and cache for Iridium security scanning
|
|
5
|
+
Project-URL: Homepage, https://github.com/mziqudhd92/Iridium
|
|
6
|
+
Project-URL: Repository, https://github.com/mziqudhd92/Iridium
|
|
7
|
+
Project-URL: Documentation, https://github.com/mziqudhd92/Iridium#readme
|
|
8
|
+
Author: Iridium Contributors
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: dependency-analysis,reachability,sast,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Requires-Dist: pathspec>=0.12
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Provides-Extra: msgpack
|
|
21
|
+
Requires-Dist: msgpack>=1.0; extra == 'msgpack'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# iridium-core
|
|
25
|
+
|
|
26
|
+
Local AST extraction, dependency graph assembly, SQLite cache, and payload models for Iridium security scanning.
|
|
27
|
+
|
|
28
|
+
**No network I/O.** No httpx dependency.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install iridium-core
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Library API
|
|
37
|
+
|
|
38
|
+
### `WorkspaceIndexer`
|
|
39
|
+
|
|
40
|
+
Primary entry point — indexes a repository into a `ClientScanPayload`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from iridium_core import WorkspaceIndexer
|
|
45
|
+
|
|
46
|
+
indexer = WorkspaceIndexer(Path("/path/to/repo"))
|
|
47
|
+
payload = indexer.index()
|
|
48
|
+
|
|
49
|
+
print(payload.dependency_count, payload.entrypoint_count)
|
|
50
|
+
print(payload.languages)
|
|
51
|
+
print(payload.to_json())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Options:
|
|
55
|
+
|
|
56
|
+
| Parameter | Default | Description |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `use_cache` | `True` | SQLite AST cache (WAL, cross-mount fallback to `~/.cache/iridium`) |
|
|
59
|
+
| `use_process_pool` | `True` | ProcessPool batches for large repos (disable for small targets) |
|
|
60
|
+
| `extractors` | `DEFAULT_EXTRACTORS` | Custom `LanguageExtractor` plugins |
|
|
61
|
+
|
|
62
|
+
### Payload validation
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from iridium_core.models.payload import ClientScanPayload
|
|
66
|
+
|
|
67
|
+
validated = ClientScanPayload.model_validate(payload.model_dump())
|
|
68
|
+
api_dict = validated.to_api_dict() # ready for POST /api/v1/client/scan
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Modules
|
|
72
|
+
|
|
73
|
+
| Module | Purpose |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `iridium_core.extract` | Python/JavaScript `LanguageExtractor` plugins |
|
|
76
|
+
| `iridium_core.graph` | Tarjan SCC, bounded BFS, adjacency |
|
|
77
|
+
| `iridium_core.deps` | Lockfile parsing (pip, npm, etc.) |
|
|
78
|
+
| `iridium_core.sanitize` | Pre-AST scrubbing (PEM, JWT, API keys) |
|
|
79
|
+
| `iridium_core.cache` | Content-addressed AST cache |
|
|
80
|
+
| `iridium_core.git` | Tree hash / commit fingerprint |
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
Part of the [Iridium monorepo](https://github.com/mziqudhd92/Iridium). From repo root:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv sync
|
|
88
|
+
uv run pytest packages/iridium-core/tests -v
|
|
89
|
+
```
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# iridium-core
|
|
2
|
+
|
|
3
|
+
Local AST extraction, dependency graph assembly, SQLite cache, and payload models for Iridium security scanning.
|
|
4
|
+
|
|
5
|
+
**No network I/O.** No httpx dependency.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install iridium-core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Library API
|
|
14
|
+
|
|
15
|
+
### `WorkspaceIndexer`
|
|
16
|
+
|
|
17
|
+
Primary entry point — indexes a repository into a `ClientScanPayload`:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from iridium_core import WorkspaceIndexer
|
|
22
|
+
|
|
23
|
+
indexer = WorkspaceIndexer(Path("/path/to/repo"))
|
|
24
|
+
payload = indexer.index()
|
|
25
|
+
|
|
26
|
+
print(payload.dependency_count, payload.entrypoint_count)
|
|
27
|
+
print(payload.languages)
|
|
28
|
+
print(payload.to_json())
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
|
|
33
|
+
| Parameter | Default | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `use_cache` | `True` | SQLite AST cache (WAL, cross-mount fallback to `~/.cache/iridium`) |
|
|
36
|
+
| `use_process_pool` | `True` | ProcessPool batches for large repos (disable for small targets) |
|
|
37
|
+
| `extractors` | `DEFAULT_EXTRACTORS` | Custom `LanguageExtractor` plugins |
|
|
38
|
+
|
|
39
|
+
### Payload validation
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from iridium_core.models.payload import ClientScanPayload
|
|
43
|
+
|
|
44
|
+
validated = ClientScanPayload.model_validate(payload.model_dump())
|
|
45
|
+
api_dict = validated.to_api_dict() # ready for POST /api/v1/client/scan
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Modules
|
|
49
|
+
|
|
50
|
+
| Module | Purpose |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `iridium_core.extract` | Python/JavaScript `LanguageExtractor` plugins |
|
|
53
|
+
| `iridium_core.graph` | Tarjan SCC, bounded BFS, adjacency |
|
|
54
|
+
| `iridium_core.deps` | Lockfile parsing (pip, npm, etc.) |
|
|
55
|
+
| `iridium_core.sanitize` | Pre-AST scrubbing (PEM, JWT, API keys) |
|
|
56
|
+
| `iridium_core.cache` | Content-addressed AST cache |
|
|
57
|
+
| `iridium_core.git` | Tree hash / commit fingerprint |
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
Part of the [Iridium monorepo](https://github.com/mziqudhd92/Iridium). From repo root:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
uv sync
|
|
65
|
+
uv run pytest packages/iridium-core/tests -v
|
|
66
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "iridium-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local AST extraction, graph assembly, and cache for Iridium security scanning"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "Apache-2.0" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Iridium Contributors" }]
|
|
9
|
+
keywords = ["security", "sast", "reachability", "dependency-analysis"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: Apache Software License",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Security",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pydantic>=2.0",
|
|
20
|
+
"pathspec>=0.12",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
msgpack = ["msgpack>=1.0"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/mziqudhd92/Iridium"
|
|
28
|
+
Repository = "https://github.com/mziqudhd92/Iridium"
|
|
29
|
+
Documentation = "https://github.com/mziqudhd92/Iridium#readme"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/iridium_core"]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""SQLite AST cache with WAL and cross-mount fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import sqlite3
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
WAL_PRAGMAS = (
|
|
11
|
+
"PRAGMA journal_mode=WAL",
|
|
12
|
+
"PRAGMA busy_timeout=5000",
|
|
13
|
+
"PRAGMA mmap_size=268435456",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
CREATE_TABLE = """
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ast_cache (
|
|
18
|
+
git_tree_hash TEXT NOT NULL,
|
|
19
|
+
file_path TEXT NOT NULL,
|
|
20
|
+
content_hash TEXT NOT NULL,
|
|
21
|
+
commit_hash TEXT,
|
|
22
|
+
fragment_json TEXT NOT NULL,
|
|
23
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
24
|
+
PRIMARY KEY (git_tree_hash, file_path, content_hash)
|
|
25
|
+
)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def content_hash(data: bytes) -> str:
|
|
30
|
+
return hashlib.sha256(data).hexdigest()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _is_cross_mount(path: Path) -> bool:
|
|
34
|
+
"""Heuristic: repo on /mnt/ (WSL) or common Docker bind mounts."""
|
|
35
|
+
posix = path.as_posix()
|
|
36
|
+
if posix.startswith("/mnt/"):
|
|
37
|
+
return True
|
|
38
|
+
home = Path.home().as_posix()
|
|
39
|
+
if not posix.startswith(home) and posix.startswith("/Users/"):
|
|
40
|
+
return False
|
|
41
|
+
# On Linux docker bind mounts often under /workspace or /repo
|
|
42
|
+
for prefix in ("/workspace", "/repo", "/host_mnt"):
|
|
43
|
+
if posix.startswith(prefix):
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def resolve_cache_path(repo: Path) -> Path:
|
|
49
|
+
"""Return active cache.db path; fallback to ~/.cache/iridium on cross-mount."""
|
|
50
|
+
local = repo / ".iridium" / "cache.db"
|
|
51
|
+
if not _is_cross_mount(repo.resolve()):
|
|
52
|
+
local.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return local
|
|
54
|
+
|
|
55
|
+
repo_hash = hashlib.sha256(str(repo.resolve()).encode()).hexdigest()[:16]
|
|
56
|
+
fallback = Path.home() / ".cache" / "iridium" / f"{repo_hash}" / "cache.db"
|
|
57
|
+
fallback.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
return fallback
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AstCache:
|
|
62
|
+
"""WAL-backed AST fragment cache keyed by git tree hash."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, db_path: Path) -> None:
|
|
65
|
+
self.db_path = db_path
|
|
66
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
self._conn = sqlite3.connect(str(db_path), timeout=5.0)
|
|
68
|
+
for pragma in WAL_PRAGMAS:
|
|
69
|
+
self._conn.execute(pragma)
|
|
70
|
+
self._conn.execute(CREATE_TABLE)
|
|
71
|
+
self._conn.commit()
|
|
72
|
+
|
|
73
|
+
def get(
|
|
74
|
+
self,
|
|
75
|
+
git_tree_hash: str,
|
|
76
|
+
file_path: str,
|
|
77
|
+
file_content_hash: str,
|
|
78
|
+
) -> str | None:
|
|
79
|
+
row = self._conn.execute(
|
|
80
|
+
"SELECT fragment_json FROM ast_cache "
|
|
81
|
+
"WHERE git_tree_hash=? AND file_path=? AND content_hash=?",
|
|
82
|
+
(git_tree_hash, file_path, file_content_hash),
|
|
83
|
+
).fetchone()
|
|
84
|
+
return row[0] if row else None
|
|
85
|
+
|
|
86
|
+
def put(
|
|
87
|
+
self,
|
|
88
|
+
git_tree_hash: str,
|
|
89
|
+
file_path: str,
|
|
90
|
+
file_content_hash: str,
|
|
91
|
+
fragment_json: str,
|
|
92
|
+
commit_hash: str | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._conn.execute(
|
|
95
|
+
"INSERT OR REPLACE INTO ast_cache "
|
|
96
|
+
"(git_tree_hash, file_path, content_hash, commit_hash, fragment_json) "
|
|
97
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
98
|
+
(git_tree_hash, file_path, file_content_hash, commit_hash, fragment_json),
|
|
99
|
+
)
|
|
100
|
+
self._conn.commit()
|
|
101
|
+
|
|
102
|
+
def close(self) -> None:
|
|
103
|
+
self._conn.close()
|
|
104
|
+
|
|
105
|
+
def __enter__(self) -> AstCache:
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def __exit__(self, *args: object) -> None:
|
|
109
|
+
self.close()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Lockfile parsers for dependency resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from iridium_core.models.payload import DependencyNode
|
|
10
|
+
|
|
11
|
+
DETERMINISM_PATTERNS = (r"\*", r"latest", r"^$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_uv_lock(repo: Path) -> tuple[list[DependencyNode], list[str]]:
|
|
15
|
+
"""Parse uv.lock TOML-like format (basic)."""
|
|
16
|
+
lock_path = repo / "uv.lock"
|
|
17
|
+
if not lock_path.is_file():
|
|
18
|
+
return [], []
|
|
19
|
+
|
|
20
|
+
warnings: list[str] = []
|
|
21
|
+
deps: list[DependencyNode] = []
|
|
22
|
+
try:
|
|
23
|
+
text = lock_path.read_text(encoding="utf-8", errors="ignore")
|
|
24
|
+
except OSError:
|
|
25
|
+
return [], []
|
|
26
|
+
|
|
27
|
+
current_name: str | None = None
|
|
28
|
+
current_version: str | None = None
|
|
29
|
+
for line in text.splitlines():
|
|
30
|
+
stripped = line.strip()
|
|
31
|
+
if stripped.startswith("name ="):
|
|
32
|
+
current_name = stripped.split("=", 1)[1].strip().strip('"')
|
|
33
|
+
elif stripped.startswith("version =") and current_name:
|
|
34
|
+
current_version = stripped.split("=", 1)[1].strip().strip('"')
|
|
35
|
+
deps.append(
|
|
36
|
+
DependencyNode(name=current_name, version=current_version, ecosystem="pypi")
|
|
37
|
+
)
|
|
38
|
+
current_name = None
|
|
39
|
+
current_version = None
|
|
40
|
+
|
|
41
|
+
for dep in deps:
|
|
42
|
+
if dep.version and any(re.search(p, dep.version, re.I) for p in DETERMINISM_PATTERNS):
|
|
43
|
+
warnings.append(f"DETERMINISM_WARNING: unpinned {dep.name}={dep.version} in uv.lock")
|
|
44
|
+
|
|
45
|
+
return deps, warnings
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_package_lock(repo: Path) -> tuple[list[DependencyNode], list[str]]:
|
|
49
|
+
"""Parse package-lock.json v2/v3 dependencies (basic)."""
|
|
50
|
+
lock_path = repo / "package-lock.json"
|
|
51
|
+
if not lock_path.is_file():
|
|
52
|
+
return [], []
|
|
53
|
+
|
|
54
|
+
warnings: list[str] = []
|
|
55
|
+
try:
|
|
56
|
+
payload = json.loads(lock_path.read_text(encoding="utf-8", errors="ignore"))
|
|
57
|
+
except (OSError, json.JSONDecodeError):
|
|
58
|
+
return [], []
|
|
59
|
+
|
|
60
|
+
packages = payload.get("packages") or {}
|
|
61
|
+
deps: list[DependencyNode] = []
|
|
62
|
+
for pkg_path, meta in packages.items():
|
|
63
|
+
if not isinstance(meta, dict):
|
|
64
|
+
continue
|
|
65
|
+
name = meta.get("name") or (pkg_path.split("node_modules/")[-1] if pkg_path else "")
|
|
66
|
+
if not name or name.startswith("node_modules"):
|
|
67
|
+
continue
|
|
68
|
+
version = meta.get("version")
|
|
69
|
+
resolved = meta.get("resolved")
|
|
70
|
+
if version:
|
|
71
|
+
deps.append(
|
|
72
|
+
DependencyNode(
|
|
73
|
+
name=name,
|
|
74
|
+
version=version,
|
|
75
|
+
ecosystem="npm",
|
|
76
|
+
resolved_url=resolved,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
if any(re.search(p, str(version), re.I) for p in DETERMINISM_PATTERNS):
|
|
80
|
+
warnings.append(
|
|
81
|
+
f"DETERMINISM_WARNING: unpinned {name}={version} in package-lock.json"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return deps, warnings
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_all_lockfiles(repo: Path) -> tuple[list[DependencyNode], list[str]]:
|
|
88
|
+
"""Parse available lockfiles; uv.lock takes precedence over manifests."""
|
|
89
|
+
all_deps: list[DependencyNode] = []
|
|
90
|
+
all_warnings: list[str] = []
|
|
91
|
+
|
|
92
|
+
uv_deps, uv_warn = parse_uv_lock(repo)
|
|
93
|
+
all_deps.extend(uv_deps)
|
|
94
|
+
all_warnings.extend(uv_warn)
|
|
95
|
+
|
|
96
|
+
npm_deps, npm_warn = parse_package_lock(repo)
|
|
97
|
+
all_deps.extend(npm_deps)
|
|
98
|
+
all_warnings.extend(npm_warn)
|
|
99
|
+
|
|
100
|
+
return all_deps, all_warnings
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Extractor plugins."""
|
|
2
|
+
|
|
3
|
+
from iridium_core.extract.base import ImportMap, LanguageExtractor
|
|
4
|
+
from iridium_core.extract.javascript_extractor import JavaScriptExtractor
|
|
5
|
+
from iridium_core.extract.python_extractor import PythonExtractor
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ImportMap",
|
|
9
|
+
"JavaScriptExtractor",
|
|
10
|
+
"LanguageExtractor",
|
|
11
|
+
"PythonExtractor",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
DEFAULT_EXTRACTORS: list[LanguageExtractor] = [
|
|
15
|
+
PythonExtractor(),
|
|
16
|
+
JavaScriptExtractor(),
|
|
17
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Language extractor plugin interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from iridium_core.models.fragment import GraphFragment
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImportMap(dict[str, str]):
|
|
14
|
+
"""Maps import specifier → resolved file path."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LanguageExtractor(ABC):
|
|
18
|
+
language: str
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def parse_batch_safe(self, paths: list[Path]) -> list[GraphFragment]:
|
|
22
|
+
"""Parse a batch of source files into graph fragments."""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def resolve_imports(self, repo: Path) -> ImportMap:
|
|
26
|
+
"""Build import resolution map for the repository."""
|
|
27
|
+
|
|
28
|
+
def supported_extensions(self) -> set[str]:
|
|
29
|
+
return set()
|