java-dependency-analyzer 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. java_dependency_analyzer-1.0.0/PKG-INFO +193 -0
  2. java_dependency_analyzer-1.0.0/README.md +175 -0
  3. java_dependency_analyzer-1.0.0/java_dependency_analyzer/__init__.py +11 -0
  4. java_dependency_analyzer-1.0.0/java_dependency_analyzer/cache/__init__.py +11 -0
  5. java_dependency_analyzer-1.0.0/java_dependency_analyzer/cache/db.py +101 -0
  6. java_dependency_analyzer-1.0.0/java_dependency_analyzer/cache/vulnerability_cache.py +156 -0
  7. java_dependency_analyzer-1.0.0/java_dependency_analyzer/cli.py +394 -0
  8. java_dependency_analyzer-1.0.0/java_dependency_analyzer/models/__init__.py +11 -0
  9. java_dependency_analyzer-1.0.0/java_dependency_analyzer/models/dependency.py +80 -0
  10. java_dependency_analyzer-1.0.0/java_dependency_analyzer/models/report.py +108 -0
  11. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/__init__.py +11 -0
  12. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/base.py +150 -0
  13. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/gradle_dep_tree_parser.py +125 -0
  14. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/gradle_parser.py +206 -0
  15. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/maven_dep_tree_parser.py +123 -0
  16. java_dependency_analyzer-1.0.0/java_dependency_analyzer/parsers/maven_parser.py +182 -0
  17. java_dependency_analyzer-1.0.0/java_dependency_analyzer/reporters/__init__.py +11 -0
  18. java_dependency_analyzer-1.0.0/java_dependency_analyzer/reporters/base.py +33 -0
  19. java_dependency_analyzer-1.0.0/java_dependency_analyzer/reporters/html_reporter.py +82 -0
  20. java_dependency_analyzer-1.0.0/java_dependency_analyzer/reporters/json_reporter.py +52 -0
  21. java_dependency_analyzer-1.0.0/java_dependency_analyzer/reporters/templates/report.html +406 -0
  22. java_dependency_analyzer-1.0.0/java_dependency_analyzer/resolvers/__init__.py +11 -0
  23. java_dependency_analyzer-1.0.0/java_dependency_analyzer/resolvers/transitive.py +276 -0
  24. java_dependency_analyzer-1.0.0/java_dependency_analyzer/scanners/__init__.py +11 -0
  25. java_dependency_analyzer-1.0.0/java_dependency_analyzer/scanners/base.py +102 -0
  26. java_dependency_analyzer-1.0.0/java_dependency_analyzer/scanners/ghsa_scanner.py +204 -0
  27. java_dependency_analyzer-1.0.0/java_dependency_analyzer/scanners/osv_scanner.py +167 -0
  28. java_dependency_analyzer-1.0.0/java_dependency_analyzer/util/__init__.py +11 -0
  29. java_dependency_analyzer-1.0.0/java_dependency_analyzer/util/logger.py +48 -0
  30. java_dependency_analyzer-1.0.0/pyproject.toml +37 -0
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: java-dependency-analyzer
3
+ Version: 1.0.0
4
+ Summary: Java Dependency Analyzer is a tool that inspects dependencies.
5
+ Author: Ron Webb
6
+ Author-email: ron@ronella.xyz
7
+ Requires-Python: >=3.14
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.14
10
+ Requires-Dist: beautifulsoup4 (>=4.14.3,<5.0.0)
11
+ Requires-Dist: click (>=8.3.1,<9.0.0)
12
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
13
+ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
14
+ Requires-Dist: lxml (>=6.0.2,<7.0.0)
15
+ Requires-Dist: python-dotenv (>=1.2.2,<2.0.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Java Dependency Analyzer 1.0.0
19
+
20
+ > A Python CLI tool that inspects Java dependency hierarchies in Maven and Gradle projects and reports known vulnerabilities.
21
+
22
+ ## Prerequisites
23
+
24
+ - Python `^3.14`
25
+ - [Poetry](https://python-poetry.org/) `2.2`
26
+
27
+ ## Installation
28
+
29
+ Clone the repository and install all dependencies:
30
+
31
+ ```bash
32
+ git clone <repository-url>
33
+ cd java-dependency-analyzer
34
+ poetry install
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```
40
+ jda <COMMAND> [OPTIONS] [FILE]
41
+ ```
42
+
43
+ `COMMAND` is one of `gradle` or `maven`.
44
+
45
+ ### gradle
46
+
47
+ ```
48
+ jda gradle [OPTIONS] [FILE]
49
+ ```
50
+
51
+ `FILE` is the path to a `build.gradle` or `build.gradle.kts` file.
52
+ Omit `FILE` when supplying `--dependencies`.
53
+
54
+ ### maven
55
+
56
+ ```
57
+ jda maven [OPTIONS] [FILE]
58
+ ```
59
+
60
+ `FILE` is the path to a `pom.xml` file.
61
+ Omit `FILE` when supplying `--dependencies`.
62
+
63
+ ### Options (both subcommands)
64
+
65
+ | Option | Short | Default | Description |
66
+ |---|---|---|---|
67
+ | `--dependencies` | `-d` | | Path to a pre-resolved dependency tree text file (see below). When supplied, parsing and transitive resolution are skipped. |
68
+ | `--output-format` | `-f` | `all` | Report format: `json`, `html`, or `all` (both). |
69
+ | `--output-dir` | `-o` | `.` | Directory to write the report file(s) into. |
70
+ | `--no-transitive` | | `false` | Skip transitive dependency resolution; analyse direct dependencies only. |
71
+ | `--verbose` | `-v` | `false` | Print progress messages to the console. |
72
+ | `--rebuild-cache` | | `false` | Delete the vulnerability cache before scanning. |
73
+ | `--cache-ttl` | | `7` | Cache TTL in days. Set to `0` to disable caching. |
74
+
75
+ ### Pre-resolved dependency trees (`--dependencies`)
76
+
77
+ When a Gradle or Maven project already has a dependency tree available (e.g. from CI), you can pass it directly to skip the parser and transitive resolver:
78
+
79
+ - **Gradle**: generate with `gradle dependencies --configuration runtimeClasspath > gradle.txt`
80
+ - **Maven**: generate with `mvn dependency:tree -Dscope=runtime > maven.txt`
81
+
82
+ The report will reflect the exact tree from the file, including all transitive dependencies.
83
+
84
+ ### Examples
85
+
86
+ Analyse a Maven POM and produce both JSON and HTML reports in the current directory:
87
+
88
+ ```bash
89
+ jda maven pom.xml
90
+ ```
91
+
92
+ Analyse a Gradle build file and write only an HTML report to `./reports/`:
93
+
94
+ ```bash
95
+ jda gradle build.gradle -f html -o reports/
96
+ ```
97
+
98
+ Analyse direct dependencies only, with verbose output:
99
+
100
+ ```bash
101
+ jda gradle build.gradle.kts --no-transitive -v
102
+ ```
103
+
104
+ Scan using a pre-resolved Gradle dependency tree (skips transitive resolution):
105
+
106
+ ```bash
107
+ jda gradle --dependencies runtime.txt -f json -o reports/
108
+ ```
109
+
110
+ Scan using a pre-resolved Maven dependency tree (skips transitive resolution):
111
+
112
+ ```bash
113
+ jda maven --dependencies maven.txt -f json -o reports/
114
+ ```
115
+
116
+ ## Architecture
117
+
118
+ ```mermaid
119
+ graph TD
120
+ CLI["jda CLI (cli.py)"] --> Parser["DependencyParser (ABC)"]
121
+ Parser --> MavenParser
122
+ Parser --> GradleParser
123
+ Parser --> MavenDepTreeParser
124
+ Parser --> GradleDepTreeParser
125
+ CLI --> Resolver["TransitiveResolver<br/>(Maven Central)"]
126
+ CLI --> Scanner["VulnerabilityScanner (ABC)"]
127
+ Scanner --> OsvScanner["OsvScanner<br/>(OSV.dev API)"]
128
+ Scanner --> GhsaScanner["GhsaScanner<br/>(GitHub Advisory DB)"]
129
+ OsvScanner --> Cache["VulnerabilityCache<br/>(SQLite)"]
130
+ GhsaScanner --> Cache
131
+ CLI --> Reporter["Reporter (ABC)"]
132
+ Reporter --> JsonReporter
133
+ Reporter --> HtmlReporter
134
+ MavenParser --> Dependency["Dependency / Vulnerability<br/>Dataclasses"]
135
+ GradleParser --> Dependency
136
+ MavenDepTreeParser --> Dependency
137
+ GradleDepTreeParser --> Dependency
138
+ Resolver --> Dependency
139
+ OsvScanner --> Dependency
140
+ GhsaScanner --> Dependency
141
+ JsonReporter --> ScanResult["ScanResult"]
142
+ HtmlReporter --> ScanResult
143
+ Dependency --> ScanResult
144
+ ```
145
+
146
+ ### Components
147
+
148
+ | Component | Location | Responsibility |
149
+ |---|---|---|
150
+ | CLI | `java_dependency_analyzer/cli.py` | Entry point (`gradle` / `maven` subcommands); orchestrates parsing, resolving, scanning, and reporting. |
151
+ | `MavenParser` | `parsers/maven_parser.py` | Parses `pom.xml`, resolves `${property}` placeholders, filters by runtime scope. |
152
+ | `GradleParser` | `parsers/gradle_parser.py` | Parses Groovy DSL (`build.gradle`) and Kotlin DSL (`build.gradle.kts`) files. |
153
+ | `MavenDepTreeParser` | `parsers/maven_dep_tree_parser.py` | Parses `mvn dependency:tree` text output into a full dependency tree. |
154
+ | `GradleDepTreeParser` | `parsers/gradle_dep_tree_parser.py` | Parses `gradle dependencies` text output into a full dependency tree. |
155
+ | `TransitiveResolver` | `resolvers/transitive.py` | Fetches transitive dependencies by downloading POM files from Maven Central. |
156
+ | `OsvScanner` | `scanners/osv_scanner.py` | Queries the [OSV.dev](https://osv.dev/) batch API for known CVEs. |
157
+ | `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories. |
158
+ | `VulnerabilityCache` | `cache/vulnerability_cache.py` | SQLite-backed cache for raw vulnerability API payloads with configurable TTL. |
159
+ | `DatabaseManager` | `cache/db.py` | Manages SQLite connection lifecycle and schema initialisation. |
160
+ | `JsonReporter` | `reporters/json_reporter.py` | Writes a `ScanResult` to a JSON file. |
161
+ | `HtmlReporter` | `reporters/html_reporter.py` | Renders a `ScanResult` to a styled HTML report via a Jinja2 template. |
162
+
163
+ ## Development Setup
164
+
165
+ Install all dependencies (including dev tools):
166
+
167
+ ```bash
168
+ poetry install
169
+ ```
170
+
171
+ ### Running Tests
172
+
173
+ Run the full test suite with coverage and generate an HTML report:
174
+
175
+ ```bash
176
+ poetry run pytest --cov=java_dependency_analyzer tests --cov-report html
177
+ ```
178
+
179
+ ### Code Quality
180
+
181
+ Format and lint the source code (linter must score 10/10):
182
+
183
+ ```bash
184
+ poetry run black java_dependency_analyzer
185
+ poetry run pylint java_dependency_analyzer
186
+ ```
187
+
188
+ ## [Changelog](CHANGELOG.md)
189
+
190
+ ## Author
191
+
192
+ Ron Webb &lt;ron@ronella.xyz&gt;
193
+
@@ -0,0 +1,175 @@
1
+ # Java Dependency Analyzer 1.0.0
2
+
3
+ > A Python CLI tool that inspects Java dependency hierarchies in Maven and Gradle projects and reports known vulnerabilities.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python `^3.14`
8
+ - [Poetry](https://python-poetry.org/) `2.2`
9
+
10
+ ## Installation
11
+
12
+ Clone the repository and install all dependencies:
13
+
14
+ ```bash
15
+ git clone <repository-url>
16
+ cd java-dependency-analyzer
17
+ poetry install
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ jda <COMMAND> [OPTIONS] [FILE]
24
+ ```
25
+
26
+ `COMMAND` is one of `gradle` or `maven`.
27
+
28
+ ### gradle
29
+
30
+ ```
31
+ jda gradle [OPTIONS] [FILE]
32
+ ```
33
+
34
+ `FILE` is the path to a `build.gradle` or `build.gradle.kts` file.
35
+ Omit `FILE` when supplying `--dependencies`.
36
+
37
+ ### maven
38
+
39
+ ```
40
+ jda maven [OPTIONS] [FILE]
41
+ ```
42
+
43
+ `FILE` is the path to a `pom.xml` file.
44
+ Omit `FILE` when supplying `--dependencies`.
45
+
46
+ ### Options (both subcommands)
47
+
48
+ | Option | Short | Default | Description |
49
+ |---|---|---|---|
50
+ | `--dependencies` | `-d` | | Path to a pre-resolved dependency tree text file (see below). When supplied, parsing and transitive resolution are skipped. |
51
+ | `--output-format` | `-f` | `all` | Report format: `json`, `html`, or `all` (both). |
52
+ | `--output-dir` | `-o` | `.` | Directory to write the report file(s) into. |
53
+ | `--no-transitive` | | `false` | Skip transitive dependency resolution; analyse direct dependencies only. |
54
+ | `--verbose` | `-v` | `false` | Print progress messages to the console. |
55
+ | `--rebuild-cache` | | `false` | Delete the vulnerability cache before scanning. |
56
+ | `--cache-ttl` | | `7` | Cache TTL in days. Set to `0` to disable caching. |
57
+
58
+ ### Pre-resolved dependency trees (`--dependencies`)
59
+
60
+ When a Gradle or Maven project already has a dependency tree available (e.g. from CI), you can pass it directly to skip the parser and transitive resolver:
61
+
62
+ - **Gradle**: generate with `gradle dependencies --configuration runtimeClasspath > gradle.txt`
63
+ - **Maven**: generate with `mvn dependency:tree -Dscope=runtime > maven.txt`
64
+
65
+ The report will reflect the exact tree from the file, including all transitive dependencies.
66
+
67
+ ### Examples
68
+
69
+ Analyse a Maven POM and produce both JSON and HTML reports in the current directory:
70
+
71
+ ```bash
72
+ jda maven pom.xml
73
+ ```
74
+
75
+ Analyse a Gradle build file and write only an HTML report to `./reports/`:
76
+
77
+ ```bash
78
+ jda gradle build.gradle -f html -o reports/
79
+ ```
80
+
81
+ Analyse direct dependencies only, with verbose output:
82
+
83
+ ```bash
84
+ jda gradle build.gradle.kts --no-transitive -v
85
+ ```
86
+
87
+ Scan using a pre-resolved Gradle dependency tree (skips transitive resolution):
88
+
89
+ ```bash
90
+ jda gradle --dependencies runtime.txt -f json -o reports/
91
+ ```
92
+
93
+ Scan using a pre-resolved Maven dependency tree (skips transitive resolution):
94
+
95
+ ```bash
96
+ jda maven --dependencies maven.txt -f json -o reports/
97
+ ```
98
+
99
+ ## Architecture
100
+
101
+ ```mermaid
102
+ graph TD
103
+ CLI["jda CLI (cli.py)"] --> Parser["DependencyParser (ABC)"]
104
+ Parser --> MavenParser
105
+ Parser --> GradleParser
106
+ Parser --> MavenDepTreeParser
107
+ Parser --> GradleDepTreeParser
108
+ CLI --> Resolver["TransitiveResolver<br/>(Maven Central)"]
109
+ CLI --> Scanner["VulnerabilityScanner (ABC)"]
110
+ Scanner --> OsvScanner["OsvScanner<br/>(OSV.dev API)"]
111
+ Scanner --> GhsaScanner["GhsaScanner<br/>(GitHub Advisory DB)"]
112
+ OsvScanner --> Cache["VulnerabilityCache<br/>(SQLite)"]
113
+ GhsaScanner --> Cache
114
+ CLI --> Reporter["Reporter (ABC)"]
115
+ Reporter --> JsonReporter
116
+ Reporter --> HtmlReporter
117
+ MavenParser --> Dependency["Dependency / Vulnerability<br/>Dataclasses"]
118
+ GradleParser --> Dependency
119
+ MavenDepTreeParser --> Dependency
120
+ GradleDepTreeParser --> Dependency
121
+ Resolver --> Dependency
122
+ OsvScanner --> Dependency
123
+ GhsaScanner --> Dependency
124
+ JsonReporter --> ScanResult["ScanResult"]
125
+ HtmlReporter --> ScanResult
126
+ Dependency --> ScanResult
127
+ ```
128
+
129
+ ### Components
130
+
131
+ | Component | Location | Responsibility |
132
+ |---|---|---|
133
+ | CLI | `java_dependency_analyzer/cli.py` | Entry point (`gradle` / `maven` subcommands); orchestrates parsing, resolving, scanning, and reporting. |
134
+ | `MavenParser` | `parsers/maven_parser.py` | Parses `pom.xml`, resolves `${property}` placeholders, filters by runtime scope. |
135
+ | `GradleParser` | `parsers/gradle_parser.py` | Parses Groovy DSL (`build.gradle`) and Kotlin DSL (`build.gradle.kts`) files. |
136
+ | `MavenDepTreeParser` | `parsers/maven_dep_tree_parser.py` | Parses `mvn dependency:tree` text output into a full dependency tree. |
137
+ | `GradleDepTreeParser` | `parsers/gradle_dep_tree_parser.py` | Parses `gradle dependencies` text output into a full dependency tree. |
138
+ | `TransitiveResolver` | `resolvers/transitive.py` | Fetches transitive dependencies by downloading POM files from Maven Central. |
139
+ | `OsvScanner` | `scanners/osv_scanner.py` | Queries the [OSV.dev](https://osv.dev/) batch API for known CVEs. |
140
+ | `GhsaScanner` | `scanners/ghsa_scanner.py` | Queries the [GitHub Advisory Database](https://github.com/advisories) REST API for security advisories. |
141
+ | `VulnerabilityCache` | `cache/vulnerability_cache.py` | SQLite-backed cache for raw vulnerability API payloads with configurable TTL. |
142
+ | `DatabaseManager` | `cache/db.py` | Manages SQLite connection lifecycle and schema initialisation. |
143
+ | `JsonReporter` | `reporters/json_reporter.py` | Writes a `ScanResult` to a JSON file. |
144
+ | `HtmlReporter` | `reporters/html_reporter.py` | Renders a `ScanResult` to a styled HTML report via a Jinja2 template. |
145
+
146
+ ## Development Setup
147
+
148
+ Install all dependencies (including dev tools):
149
+
150
+ ```bash
151
+ poetry install
152
+ ```
153
+
154
+ ### Running Tests
155
+
156
+ Run the full test suite with coverage and generate an HTML report:
157
+
158
+ ```bash
159
+ poetry run pytest --cov=java_dependency_analyzer tests --cov-report html
160
+ ```
161
+
162
+ ### Code Quality
163
+
164
+ Format and lint the source code (linter must score 10/10):
165
+
166
+ ```bash
167
+ poetry run black java_dependency_analyzer
168
+ poetry run pylint java_dependency_analyzer
169
+ ```
170
+
171
+ ## [Changelog](CHANGELOG.md)
172
+
173
+ ## Author
174
+
175
+ Ron Webb &lt;ron@ronella.xyz&gt;
@@ -0,0 +1,11 @@
1
+ """
2
+ java_dependency_analyzer package.
3
+
4
+ Java Dependency Analyzer is a tool that inspects dependencies.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ __author__ = "Ron Webb"
11
+ __since__ = "1.0.0"
@@ -0,0 +1,11 @@
1
+ """
2
+ cache package.
3
+
4
+ Provides SQLite-backed persistence for vulnerability scan results.
5
+
6
+ :author: Ron Webb
7
+ :since: 1.0.0
8
+ """
9
+
10
+ __author__ = "Ron Webb"
11
+ __since__ = "1.0.0"
@@ -0,0 +1,101 @@
1
+ """
2
+ db module.
3
+
4
+ Manages the SQLite database lifecycle: path resolution, connection,
5
+ schema creation, and deletion.
6
+
7
+ :author: Ron Webb
8
+ :since: 1.0.0
9
+ """
10
+
11
+ import sqlite3
12
+ from pathlib import Path
13
+
14
+ from ..util.logger import setup_logger
15
+
16
+ __author__ = "Ron Webb"
17
+ __since__ = "1.0.0"
18
+
19
+ _logger = setup_logger(__name__)
20
+
21
+ _DB_DIR = Path.home() / ".jda"
22
+ _DB_FILE = "cache.db"
23
+
24
+ _CREATE_TABLE_SQL = """
25
+ CREATE TABLE IF NOT EXISTS vulnerability_cache (
26
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
27
+ source TEXT NOT NULL,
28
+ group_id TEXT NOT NULL,
29
+ artifact_id TEXT NOT NULL,
30
+ version TEXT NOT NULL,
31
+ payload TEXT NOT NULL,
32
+ cached_at TEXT NOT NULL,
33
+ UNIQUE(source, group_id, artifact_id, version)
34
+ )
35
+ """
36
+
37
+ _CREATE_INDEX_SQL = """
38
+ CREATE INDEX IF NOT EXISTS idx_cache_lookup
39
+ ON vulnerability_cache(group_id, artifact_id, version, source)
40
+ """
41
+
42
+
43
+ def get_db_path() -> Path:
44
+ """
45
+ Return the absolute path to the SQLite cache database file.
46
+
47
+ The default location is ``~/.jda/cache.db``.
48
+
49
+ :author: Ron Webb
50
+ :since: 1.0.0
51
+ """
52
+ return _DB_DIR / _DB_FILE
53
+
54
+
55
+ def get_connection() -> sqlite3.Connection:
56
+ """
57
+ Open (and initialise) the SQLite database, creating the parent directory
58
+ and schema if they do not yet exist.
59
+
60
+ Returns a ``sqlite3.Connection`` with ``check_same_thread=False`` so the
61
+ connection can be shared within a single CLI invocation.
62
+
63
+ :author: Ron Webb
64
+ :since: 1.0.0
65
+ """
66
+ db_path = get_db_path()
67
+ db_path.parent.mkdir(parents=True, exist_ok=True)
68
+ _logger.debug("Opening cache database at %s", db_path)
69
+ conn = sqlite3.connect(str(db_path), check_same_thread=False)
70
+ _initialise_schema(conn)
71
+ return conn
72
+
73
+
74
+ def _initialise_schema(conn: sqlite3.Connection) -> None:
75
+ """
76
+ Create the cache table and lookup index when they do not already exist.
77
+
78
+ :author: Ron Webb
79
+ :since: 1.0.0
80
+ """
81
+ with conn:
82
+ conn.execute(_CREATE_TABLE_SQL)
83
+ conn.execute(_CREATE_INDEX_SQL)
84
+
85
+
86
+ def delete_database() -> bool:
87
+ """
88
+ Delete the SQLite cache database file.
89
+
90
+ Returns ``True`` if the file was deleted, ``False`` if it did not exist.
91
+
92
+ :author: Ron Webb
93
+ :since: 1.0.0
94
+ """
95
+ db_path = get_db_path()
96
+ if db_path.exists():
97
+ db_path.unlink()
98
+ _logger.info("Deleted cache database at %s", db_path)
99
+ return True
100
+ _logger.debug("Cache database not found at %s, nothing to delete", db_path)
101
+ return False
@@ -0,0 +1,156 @@
1
+ """
2
+ vulnerability_cache module.
3
+
4
+ Provides a SQLite-backed cache for vulnerability scan API responses.
5
+ Cache entries expire after a configurable number of days (default: 7).
6
+
7
+ :author: Ron Webb
8
+ :since: 1.0.0
9
+ """
10
+
11
+ import sqlite3
12
+ from datetime import datetime, timedelta
13
+
14
+ from ..util.logger import setup_logger
15
+ from .db import get_connection
16
+
17
+ __author__ = "Ron Webb"
18
+ __since__ = "1.0.0"
19
+
20
+ _logger = setup_logger(__name__)
21
+
22
+ _DEFAULT_TTL_DAYS = 7
23
+
24
+ _SELECT_SQL = """
25
+ SELECT payload, cached_at FROM vulnerability_cache
26
+ WHERE source = ? AND group_id = ? AND artifact_id = ? AND version = ?
27
+ """
28
+
29
+ _UPSERT_SQL = """
30
+ INSERT INTO vulnerability_cache(source, group_id, artifact_id, version, payload, cached_at)
31
+ VALUES (?, ?, ?, ?, ?, ?)
32
+ ON CONFLICT(source, group_id, artifact_id, version)
33
+ DO UPDATE SET payload = excluded.payload, cached_at = excluded.cached_at
34
+ """
35
+
36
+ _DELETE_SQL = """
37
+ DELETE FROM vulnerability_cache
38
+ WHERE source = ? AND group_id = ? AND artifact_id = ? AND version = ?
39
+ """
40
+
41
+
42
+ class VulnerabilityCache:
43
+ """
44
+ SQLite-backed cache for raw vulnerability API payloads.
45
+
46
+ Each entry is keyed by ``(source, group_id, artifact_id, version)`` and stores
47
+ the raw JSON response payload string. Entries older than ``ttl_days`` are
48
+ treated as expired and will not be returned.
49
+
50
+ :author: Ron Webb
51
+ :since: 1.0.0
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ connection: sqlite3.Connection | None = None,
57
+ ttl_days: int = _DEFAULT_TTL_DAYS,
58
+ ) -> None:
59
+ """
60
+ Initialise the cache with an optional SQLite connection and TTL.
61
+
62
+ When *connection* is ``None`` the default on-disk database is used.
63
+
64
+ :author: Ron Webb
65
+ :since: 1.0.0
66
+ """
67
+ self._conn = connection or get_connection()
68
+ self._ttl_days = ttl_days
69
+
70
+ def get(
71
+ self,
72
+ source: str,
73
+ group_id: str,
74
+ artifact_id: str,
75
+ version: str,
76
+ ) -> str | None:
77
+ """
78
+ Return the cached JSON payload for the given key, or ``None`` when there
79
+ is no valid (non-expired) entry.
80
+
81
+ :author: Ron Webb
82
+ :since: 1.0.0
83
+ """
84
+ cursor = self._conn.execute(_SELECT_SQL, (source, group_id, artifact_id, version))
85
+ row = cursor.fetchone()
86
+ if row is None:
87
+ return None
88
+ payload, cached_at = row
89
+ if self._is_expired(cached_at):
90
+ with self._conn:
91
+ self._conn.execute(_DELETE_SQL, (source, group_id, artifact_id, version))
92
+ _logger.debug(
93
+ "Cache entry expired and deleted for %s/%s:%s@%s",
94
+ source,
95
+ group_id,
96
+ artifact_id,
97
+ version,
98
+ )
99
+ return None
100
+ _logger.debug(
101
+ "Cache hit for %s/%s:%s@%s", source, group_id, artifact_id, version
102
+ )
103
+ return payload
104
+
105
+ def put( # pylint: disable=too-many-arguments,too-many-positional-arguments
106
+ self,
107
+ source: str,
108
+ group_id: str,
109
+ artifact_id: str,
110
+ version: str,
111
+ payload: str,
112
+ ) -> None:
113
+ """
114
+ Insert or replace the cache entry for the given key.
115
+
116
+ *payload* must be a JSON string representing the raw API response.
117
+
118
+ :author: Ron Webb
119
+ :since: 1.0.0
120
+ """
121
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
122
+ with self._conn:
123
+ self._conn.execute(
124
+ _UPSERT_SQL,
125
+ (source, group_id, artifact_id, version, payload, now),
126
+ )
127
+ _logger.debug(
128
+ "Cached payload for %s/%s:%s@%s", source, group_id, artifact_id, version
129
+ )
130
+
131
+ def close(self) -> None:
132
+ """
133
+ Close the underlying SQLite connection.
134
+
135
+ Call this when the cache is no longer needed to release the database
136
+ file handle immediately. Callers that passed their own *connection* to
137
+ the constructor are responsible for not using the connection after calling
138
+ this method.
139
+
140
+ :author: Ron Webb
141
+ :since: 1.0.0
142
+ """
143
+ self._conn.close()
144
+
145
+ def _is_expired(self, cached_at: str) -> bool:
146
+ """
147
+ Return ``True`` when *cached_at* is older than the configured TTL.
148
+
149
+ :author: Ron Webb
150
+ :since: 1.0.0
151
+ """
152
+ try:
153
+ entry_time = datetime.strptime(cached_at, "%Y-%m-%d %H:%M:%S")
154
+ except ValueError:
155
+ return True
156
+ return datetime.now() - entry_time > timedelta(days=self._ttl_days)