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.
Files changed (43) hide show
  1. iridium_core-0.1.0/.gitignore +33 -0
  2. iridium_core-0.1.0/PKG-INFO +89 -0
  3. iridium_core-0.1.0/README.md +66 -0
  4. iridium_core-0.1.0/pyproject.toml +36 -0
  5. iridium_core-0.1.0/src/iridium_core/__init__.py +6 -0
  6. iridium_core-0.1.0/src/iridium_core/cache/__init__.py +5 -0
  7. iridium_core-0.1.0/src/iridium_core/cache/ast_cache.py +109 -0
  8. iridium_core-0.1.0/src/iridium_core/deps/__init__.py +5 -0
  9. iridium_core-0.1.0/src/iridium_core/deps/lockfiles.py +100 -0
  10. iridium_core-0.1.0/src/iridium_core/extract/__init__.py +17 -0
  11. iridium_core-0.1.0/src/iridium_core/extract/base.py +29 -0
  12. iridium_core-0.1.0/src/iridium_core/extract/javascript_extractor.py +199 -0
  13. iridium_core-0.1.0/src/iridium_core/extract/python_extractor.py +285 -0
  14. iridium_core-0.1.0/src/iridium_core/git/__init__.py +15 -0
  15. iridium_core-0.1.0/src/iridium_core/git/tree_hash.py +58 -0
  16. iridium_core-0.1.0/src/iridium_core/graph/__init__.py +12 -0
  17. iridium_core-0.1.0/src/iridium_core/graph/bfs.py +67 -0
  18. iridium_core-0.1.0/src/iridium_core/graph/scc.py +78 -0
  19. iridium_core-0.1.0/src/iridium_core/ignore/__init__.py +5 -0
  20. iridium_core-0.1.0/src/iridium_core/ignore/matcher.py +44 -0
  21. iridium_core-0.1.0/src/iridium_core/models/__init__.py +15 -0
  22. iridium_core-0.1.0/src/iridium_core/models/enums.py +22 -0
  23. iridium_core-0.1.0/src/iridium_core/models/fragment.py +35 -0
  24. iridium_core-0.1.0/src/iridium_core/models/payload.py +109 -0
  25. iridium_core-0.1.0/src/iridium_core/sandbox/__init__.py +17 -0
  26. iridium_core-0.1.0/src/iridium_core/sandbox/pool.py +77 -0
  27. iridium_core-0.1.0/src/iridium_core/sanitize/__init__.py +5 -0
  28. iridium_core-0.1.0/src/iridium_core/sanitize/scrubber.py +44 -0
  29. iridium_core-0.1.0/src/iridium_core/workspace/__init__.py +5 -0
  30. iridium_core-0.1.0/src/iridium_core/workspace/indexer.py +177 -0
  31. iridium_core-0.1.0/tests/test_backend_payload_compat.py +106 -0
  32. iridium_core-0.1.0/tests/test_cache.py +36 -0
  33. iridium_core-0.1.0/tests/test_deps.py +46 -0
  34. iridium_core-0.1.0/tests/test_extract_javascript.py +60 -0
  35. iridium_core-0.1.0/tests/test_extract_python.py +67 -0
  36. iridium_core-0.1.0/tests/test_git.py +20 -0
  37. iridium_core-0.1.0/tests/test_graph.py +40 -0
  38. iridium_core-0.1.0/tests/test_graph_extended.py +18 -0
  39. iridium_core-0.1.0/tests/test_ignore.py +23 -0
  40. iridium_core-0.1.0/tests/test_indexer.py +40 -0
  41. iridium_core-0.1.0/tests/test_payload.py +105 -0
  42. iridium_core-0.1.0/tests/test_sandbox.py +30 -0
  43. 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,6 @@
1
+ """Iridium core — local extraction, graph assembly, and caching."""
2
+
3
+ from iridium_core.workspace.indexer import WorkspaceIndexer
4
+
5
+ __all__ = ["WorkspaceIndexer"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """AST cache."""
2
+
3
+ from iridium_core.cache.ast_cache import AstCache, content_hash, resolve_cache_path
4
+
5
+ __all__ = ["AstCache", "content_hash", "resolve_cache_path"]
@@ -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,5 @@
1
+ """Dependency lockfile parsing."""
2
+
3
+ from iridium_core.deps.lockfiles import parse_all_lockfiles, parse_package_lock, parse_uv_lock
4
+
5
+ __all__ = ["parse_all_lockfiles", "parse_package_lock", "parse_uv_lock"]
@@ -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()