depsdev 0.0.2__tar.gz → 0.0.4__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.
- {depsdev-0.0.2 → depsdev-0.0.4}/PKG-INFO +4 -1
- {depsdev-0.0.2 → depsdev-0.0.4}/pyproject.toml +2 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/__main__.py +73 -24
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/_version.py +2 -2
- depsdev-0.0.4/src/depsdev/base.py +47 -0
- depsdev-0.0.4/src/depsdev/cli/purl.py +142 -0
- depsdev-0.0.4/src/depsdev/cli/vuln.py +71 -0
- depsdev-0.0.4/src/depsdev/osv.py +195 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/v3.py +8 -1
- depsdev-0.0.4/tests/__init__.py +0 -0
- depsdev-0.0.4/tests/v3_test.py +28 -0
- depsdev-0.0.4/tests/v3alpha_test.py +50 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/.github/copilot-instructions.md +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/.github/workflows/main.yaml +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/.gitignore +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/.pre-commit-config.yaml +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/.python-version +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/LICENSE.txt +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/README.md +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/__init__.py +0 -0
- {depsdev-0.0.2/tests → depsdev-0.0.4/src/depsdev/cli}/__init__.py +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/py.typed +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/src/depsdev/v3alpha.py +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/taplo.toml +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/tests/main_test.py +0 -0
- {depsdev-0.0.2 → depsdev-0.0.4}/tests/scripts_test.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: depsdev
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.4
|
4
4
|
Summary: Python wrapper for https://deps.dev/ API
|
5
5
|
Project-URL: Documentation, https://github.com/FlavioAmurrioCS/depsdev#readme
|
6
6
|
Project-URL: Issues, https://github.com/FlavioAmurrioCS/depsdev/issues
|
@@ -20,11 +20,13 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
20
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
21
21
|
Requires-Python: >=3.9
|
22
22
|
Requires-Dist: httpx
|
23
|
+
Requires-Dist: packageurl-python
|
23
24
|
Provides-Extra: cli
|
24
25
|
Requires-Dist: rich; extra == 'cli'
|
25
26
|
Requires-Dist: typer-slim; extra == 'cli'
|
26
27
|
Provides-Extra: tests
|
27
28
|
Requires-Dist: pytest; extra == 'tests'
|
29
|
+
Requires-Dist: pytest-asyncio; extra == 'tests'
|
28
30
|
Requires-Dist: rich; extra == 'tests'
|
29
31
|
Requires-Dist: tomli; (python_version < '3.11') and extra == 'tests'
|
30
32
|
Requires-Dist: typer-slim; extra == 'tests'
|
@@ -33,6 +35,7 @@ Requires-Dist: mypy; extra == 'types'
|
|
33
35
|
Requires-Dist: pyrefly; extra == 'types'
|
34
36
|
Requires-Dist: pyright[nodejs]; extra == 'types'
|
35
37
|
Requires-Dist: pytest; extra == 'types'
|
38
|
+
Requires-Dist: pytest-asyncio; extra == 'types'
|
36
39
|
Requires-Dist: rich; extra == 'types'
|
37
40
|
Requires-Dist: tomli; (python_version < '3.11') and extra == 'types'
|
38
41
|
Requires-Dist: ty; extra == 'types'
|
@@ -27,6 +27,7 @@ classifiers = [
|
|
27
27
|
]
|
28
28
|
dependencies = [
|
29
29
|
"httpx",
|
30
|
+
"packageurl-python",
|
30
31
|
]
|
31
32
|
|
32
33
|
[project.optional-dependencies]
|
@@ -38,6 +39,7 @@ tests = [
|
|
38
39
|
"pytest",
|
39
40
|
"tomli ; python_version < '3.11'",
|
40
41
|
"depsdev[cli]",
|
42
|
+
"pytest-asyncio",
|
41
43
|
]
|
42
44
|
types = [
|
43
45
|
"depsdev[tests]",
|
@@ -1,5 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import logging
|
4
|
+
import sys
|
5
|
+
|
6
|
+
from depsdev.cli.purl import get_extractor
|
7
|
+
from depsdev.cli.vuln import main_helper
|
8
|
+
|
9
|
+
try:
|
10
|
+
import typer
|
11
|
+
except ImportError:
|
12
|
+
msg = (
|
13
|
+
"The 'cli' optional dependency is not installed. "
|
14
|
+
"Please install it with 'pip install depsdev[cli]'."
|
15
|
+
)
|
16
|
+
print(msg, file=sys.stderr)
|
17
|
+
raise SystemExit(1) from None
|
18
|
+
|
3
19
|
import os
|
4
20
|
from textwrap import dedent
|
5
21
|
from typing import TYPE_CHECKING
|
@@ -13,6 +29,13 @@ if TYPE_CHECKING:
|
|
13
29
|
P = ParamSpec("P")
|
14
30
|
R = TypeVar("R")
|
15
31
|
|
32
|
+
logging.basicConfig(
|
33
|
+
level=logging.ERROR,
|
34
|
+
format="[%(asctime)s] [%(levelname)-7s] [%(name)s] %(message)s",
|
35
|
+
)
|
36
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
37
|
+
logger = logging.getLogger("depsdev")
|
38
|
+
|
16
39
|
|
17
40
|
def to_sync() -> Callable[[Callable[P, R]], Callable[P, R]]:
|
18
41
|
"""
|
@@ -29,8 +52,11 @@ def to_sync() -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
29
52
|
|
30
53
|
from rich import print_json
|
31
54
|
|
32
|
-
|
33
|
-
|
55
|
+
result: object = asyncio.run(func(*args, **kwargs)) # type: ignore[arg-type]
|
56
|
+
if result is not None:
|
57
|
+
print_json(data=result)
|
58
|
+
except Exception:
|
59
|
+
logger.exception("An error occurred while executing the command.")
|
34
60
|
raise SystemExit(1) from None
|
35
61
|
|
36
62
|
raise SystemExit(0)
|
@@ -40,33 +66,14 @@ def to_sync() -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
40
66
|
return decorator
|
41
67
|
|
42
68
|
|
43
|
-
def
|
69
|
+
def create_app() -> typer.Typer:
|
44
70
|
"""
|
45
71
|
Main entry point for the CLI.
|
46
72
|
"""
|
47
|
-
import logging
|
48
|
-
|
49
|
-
logging.basicConfig(
|
50
|
-
level=logging.ERROR,
|
51
|
-
format="[%(asctime)s] [%(levelname)-7s] [%(name)s] %(message)s",
|
52
|
-
)
|
53
|
-
logging.getLogger("httpx").setLevel(logging.WARNING)
|
54
|
-
logger = logging.getLogger("depsdev")
|
55
|
-
|
56
|
-
try:
|
57
|
-
import typer
|
58
|
-
except ImportError:
|
59
|
-
msg = (
|
60
|
-
"The 'cli' optional dependency is not installed. "
|
61
|
-
"Please install it with 'pip install depsdev[cli]'."
|
62
|
-
)
|
63
|
-
logger.error(msg) # noqa: TRY400
|
64
|
-
raise SystemExit(1) from None
|
65
|
-
|
66
73
|
alpha = os.environ.get("DEPSDEV_V3_ALPHA", "false").lower() in ("true", "1", "yes")
|
67
74
|
|
68
75
|
app = typer.Typer(
|
69
|
-
name="
|
76
|
+
name="api",
|
70
77
|
no_args_is_help=True,
|
71
78
|
rich_markup_mode="rich",
|
72
79
|
help=dedent(
|
@@ -123,7 +130,49 @@ def main() -> None:
|
|
123
130
|
app.command(rich_help_panel="v3alpha")(to_sync()(client_v3_alpha.purl_lookup_batch))
|
124
131
|
app.command(rich_help_panel="v3alpha")(to_sync()(client_v3_alpha.query_container_images))
|
125
132
|
|
126
|
-
return app
|
133
|
+
return app
|
134
|
+
|
135
|
+
|
136
|
+
main = typer.Typer(
|
137
|
+
name="depsdev",
|
138
|
+
no_args_is_help=True,
|
139
|
+
rich_markup_mode="rich",
|
140
|
+
)
|
141
|
+
|
142
|
+
main.add_typer(
|
143
|
+
create_app(),
|
144
|
+
name="api",
|
145
|
+
)
|
146
|
+
|
147
|
+
|
148
|
+
@main.command(name="purl", rich_help_panel="Utils")
|
149
|
+
def purl(filename: str) -> None:
|
150
|
+
"""
|
151
|
+
Extract package URLs from various formats.
|
152
|
+
"""
|
153
|
+
extractor = get_extractor(filename)
|
154
|
+
|
155
|
+
for purl in extractor.extract(filename):
|
156
|
+
print(purl)
|
157
|
+
|
158
|
+
|
159
|
+
main.command(name="vuln", rich_help_panel="Utils")(to_sync()(main_helper))
|
160
|
+
|
161
|
+
|
162
|
+
@main.command()
|
163
|
+
@to_sync()
|
164
|
+
async def report(filename: str) -> None:
|
165
|
+
"""
|
166
|
+
Show vulnerabilities for packages in a file.
|
167
|
+
|
168
|
+
Example usage:
|
169
|
+
depsdev report requirements.txt
|
170
|
+
depsdev report pom.xml
|
171
|
+
depsdev report Pipfile.lock
|
172
|
+
"""
|
173
|
+
extractor = get_extractor(filename)
|
174
|
+
packages = extractor.extract(filename)
|
175
|
+
await main_helper([x.to_string() for x in packages])
|
127
176
|
|
128
177
|
|
129
178
|
if __name__ == "__main__":
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from dataclasses import field
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
from urllib.parse import quote
|
8
|
+
|
9
|
+
import httpx
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from httpx._types import QueryParamTypes
|
13
|
+
from typing_extensions import Literal
|
14
|
+
|
15
|
+
from depsdev.v3 import Incomplete
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class BaseClient:
|
22
|
+
base_url: str
|
23
|
+
timeout: float = 5.0
|
24
|
+
client: httpx.AsyncClient = field(init=False, repr=False)
|
25
|
+
|
26
|
+
def __post_init__(self) -> None:
|
27
|
+
self.client = httpx.AsyncClient(base_url=self.base_url, timeout=self.timeout)
|
28
|
+
|
29
|
+
async def _requests(
|
30
|
+
self,
|
31
|
+
url: str = "",
|
32
|
+
method: Literal["GET", "POST"] = "GET",
|
33
|
+
params: QueryParamTypes | None = None,
|
34
|
+
json: object | None = None,
|
35
|
+
) -> Incomplete:
|
36
|
+
logger.info(locals())
|
37
|
+
response = await self.client.request(method=method, url=url, params=params, json=json)
|
38
|
+
if not response.is_success:
|
39
|
+
logger.error(
|
40
|
+
"Request failed with status code %s: %s", response.status_code, response.text
|
41
|
+
)
|
42
|
+
response.raise_for_status()
|
43
|
+
return response.json()
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def url_escape(string: str) -> str:
|
47
|
+
return quote(string, safe="")
|
@@ -0,0 +1,142 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import itertools
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import subprocess
|
8
|
+
import sys
|
9
|
+
from typing import TYPE_CHECKING
|
10
|
+
|
11
|
+
from packageurl import PackageURL
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from collections.abc import Iterable
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class MavenExtractor:
|
20
|
+
@classmethod
|
21
|
+
def extract(cls, filename: str) -> Iterable[PackageURL]:
|
22
|
+
yield from (cls.parse_single_line(x) for x in cls._clean(cls._get_source(filename)))
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
def _get_source(filename: str) -> Iterable[str]:
|
26
|
+
"""
|
27
|
+
Read lines from stdin or a file.
|
28
|
+
"""
|
29
|
+
if not filename.endswith("pom.xml"):
|
30
|
+
logger.error("Invalid POM file: %s. It should end with 'pom.xml'.", filename)
|
31
|
+
raise SystemExit(1)
|
32
|
+
result = subprocess.run(
|
33
|
+
["mvn", "dependency:tree"], # noqa: S607
|
34
|
+
check=False,
|
35
|
+
capture_output=True,
|
36
|
+
text=True,
|
37
|
+
cwd=os.path.dirname(os.path.abspath(filename)),
|
38
|
+
)
|
39
|
+
if result.returncode != 0:
|
40
|
+
print(result.stderr, file=sys.stderr)
|
41
|
+
raise SystemExit(1)
|
42
|
+
yield from result.stdout.splitlines()
|
43
|
+
|
44
|
+
@staticmethod
|
45
|
+
def parse_single_line(line: str) -> PackageURL:
|
46
|
+
"""
|
47
|
+
Parse a single line of Maven dependency output and return a PackageURL object.
|
48
|
+
"""
|
49
|
+
package, *rest = line.split()
|
50
|
+
_is_optional = bool(rest)
|
51
|
+
group, artifact, _type, version, *classifier = package.split(":")
|
52
|
+
return PackageURL(
|
53
|
+
type="maven",
|
54
|
+
namespace=group,
|
55
|
+
name=artifact,
|
56
|
+
version=version,
|
57
|
+
qualifiers=None,
|
58
|
+
subpath=None,
|
59
|
+
)
|
60
|
+
|
61
|
+
@staticmethod
|
62
|
+
def _clean(lines: Iterable[str]) -> Iterable[str]:
|
63
|
+
stage1 = (x.rstrip() for x in lines if x.strip())
|
64
|
+
stage2 = itertools.dropwhile(lambda x: not x.startswith("[INFO] --- "), stage1)
|
65
|
+
stage3 = itertools.takewhile(
|
66
|
+
lambda x: not x.startswith(
|
67
|
+
"[INFO] ------------------------------------------------------------------------"
|
68
|
+
),
|
69
|
+
stage2,
|
70
|
+
)
|
71
|
+
stage4 = itertools.islice(stage3, 1, None) # Skip the first line
|
72
|
+
stage5 = (x[7:] for x in stage4)
|
73
|
+
yield from (x.split("- ", maxsplit=1)[-1] for x in stage5)
|
74
|
+
|
75
|
+
|
76
|
+
class PipfileLockExtractor:
|
77
|
+
@classmethod
|
78
|
+
def extract(cls, filename: str) -> Iterable[PackageURL]:
|
79
|
+
"""
|
80
|
+
Extracts package URLs from a Pipfile.lock.
|
81
|
+
"""
|
82
|
+
if not filename.endswith("Pipfile.lock"):
|
83
|
+
logger.error("Invalid Pipfile.lock: %s. It should end with 'Pipfile.lock'.", filename)
|
84
|
+
raise SystemExit(1)
|
85
|
+
with open(filename) as f:
|
86
|
+
data = json.load(f)
|
87
|
+
for package_name, package_info in data.get("default", {}).items():
|
88
|
+
version: str | None = package_info.get("version")
|
89
|
+
if version:
|
90
|
+
yield PackageURL(
|
91
|
+
type="pypi",
|
92
|
+
namespace=None,
|
93
|
+
name=package_name,
|
94
|
+
version=version[2:],
|
95
|
+
qualifiers=None,
|
96
|
+
subpath=None,
|
97
|
+
)
|
98
|
+
else:
|
99
|
+
logger.warning("Package %s has no version specified.", package_name)
|
100
|
+
|
101
|
+
|
102
|
+
class RequirementsExtractor:
|
103
|
+
@classmethod
|
104
|
+
def extract(cls, filename: str) -> Iterable[PackageURL]:
|
105
|
+
"""
|
106
|
+
Extracts package URLs from a requirements.txt file.
|
107
|
+
"""
|
108
|
+
if not filename.endswith("requirements.txt"):
|
109
|
+
logger.error(
|
110
|
+
"Invalid requirements file: %s. It should end with 'requirements.txt'.", filename
|
111
|
+
)
|
112
|
+
raise SystemExit(1)
|
113
|
+
with open(filename) as f:
|
114
|
+
for line in f:
|
115
|
+
_line = line.strip()
|
116
|
+
if not _line or _line.startswith(("#", "-r ", "-i ")):
|
117
|
+
continue
|
118
|
+
parts = _line.split(";")[0].split("==")
|
119
|
+
if len(parts) == 2: # noqa: PLR2004
|
120
|
+
name, version = parts
|
121
|
+
yield PackageURL(
|
122
|
+
type="pypi",
|
123
|
+
namespace=None,
|
124
|
+
name=name,
|
125
|
+
version=version,
|
126
|
+
qualifiers=None,
|
127
|
+
subpath=None,
|
128
|
+
)
|
129
|
+
|
130
|
+
|
131
|
+
def get_extractor(filename: str) -> MavenExtractor | PipfileLockExtractor | RequirementsExtractor:
|
132
|
+
"""
|
133
|
+
Returns the appropriate extractor based on the file extension.
|
134
|
+
"""
|
135
|
+
if filename.endswith("pom.xml"):
|
136
|
+
return MavenExtractor()
|
137
|
+
if filename.endswith("Pipfile.lock"):
|
138
|
+
return PipfileLockExtractor()
|
139
|
+
if filename.endswith("requirements.txt"):
|
140
|
+
return RequirementsExtractor()
|
141
|
+
logger.error("Unsupported file format: %s", filename)
|
142
|
+
raise SystemExit(1)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import itertools
|
5
|
+
import logging
|
6
|
+
from typing import TYPE_CHECKING
|
7
|
+
|
8
|
+
from rich.console import Console
|
9
|
+
from rich.table import Table
|
10
|
+
|
11
|
+
from depsdev.osv import OSVClientV1
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from depsdev.osv import OSVVulnerability
|
15
|
+
from depsdev.osv import V1Query
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
|
20
|
+
def get_version_fix(vuln: OSVVulnerability) -> str | None:
|
21
|
+
for affected in vuln.get("affected", []):
|
22
|
+
for _range in affected.get("ranges", []):
|
23
|
+
for event in _range.get("events", []):
|
24
|
+
if "fixed" in event:
|
25
|
+
return event["fixed"]
|
26
|
+
return None
|
27
|
+
|
28
|
+
|
29
|
+
async def get_vulns(purls: list[str], osv_client: OSVClientV1) -> dict[str, list[OSVVulnerability]]:
|
30
|
+
queries: list[V1Query] = [
|
31
|
+
{
|
32
|
+
"package": {"purl": purl},
|
33
|
+
}
|
34
|
+
for purl in purls
|
35
|
+
]
|
36
|
+
result = await osv_client.querybatch({"queries": queries})
|
37
|
+
r = {k: [x["id"] for x in v["vulns"]] for k, v in zip(purls, result["results"]) if v}
|
38
|
+
all_result = await asyncio.gather(
|
39
|
+
*[osv_client.get_vuln(vuln_id) for vuln_id in itertools.chain.from_iterable(r.values())]
|
40
|
+
)
|
41
|
+
look_up = {vuln["id"]: vuln for vuln in all_result}
|
42
|
+
return {purl: [look_up[vuln_id] for vuln_id in vuln_ids] for purl, vuln_ids in r.items()}
|
43
|
+
|
44
|
+
|
45
|
+
async def main_helper(packages: list[str]) -> int:
|
46
|
+
"""Main function to analyze packages for vulnerabilities."""
|
47
|
+
|
48
|
+
console = Console()
|
49
|
+
|
50
|
+
console.print(f"Analysing {len(packages)} packages...")
|
51
|
+
|
52
|
+
osv_client = OSVClientV1()
|
53
|
+
|
54
|
+
results = await get_vulns(packages, osv_client)
|
55
|
+
console.print(f"Found {len(results)} packages with advisories.")
|
56
|
+
|
57
|
+
for purl, advisories in results.items():
|
58
|
+
table = Table(title=purl)
|
59
|
+
|
60
|
+
table.add_column("Id")
|
61
|
+
table.add_column("Summary", style="cyan", no_wrap=True)
|
62
|
+
table.add_column("Fixed", style="magenta")
|
63
|
+
|
64
|
+
for vuln in advisories:
|
65
|
+
table.add_row(
|
66
|
+
f"[link=https://github.com/advisories/{vuln['id']}]{vuln['id']}[/link]",
|
67
|
+
vuln["summary"],
|
68
|
+
get_version_fix(vuln) or "unknown",
|
69
|
+
)
|
70
|
+
console.print(table)
|
71
|
+
return 0
|
@@ -0,0 +1,195 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from depsdev.base import BaseClient
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from typing import Any
|
11
|
+
from typing import Literal
|
12
|
+
|
13
|
+
from typing_extensions import NotRequired
|
14
|
+
from typing_extensions import TypedDict
|
15
|
+
|
16
|
+
class OSVEvent(TypedDict):
|
17
|
+
introduced: NotRequired[str]
|
18
|
+
fixed: NotRequired[str]
|
19
|
+
limit: NotRequired[str]
|
20
|
+
lastAffected: NotRequired[str]
|
21
|
+
|
22
|
+
class OSVRange(TypedDict):
|
23
|
+
type: NotRequired[
|
24
|
+
Literal[
|
25
|
+
"UNSPECIFIED",
|
26
|
+
"GIT",
|
27
|
+
"SEMVER",
|
28
|
+
"ECOSYSTEM",
|
29
|
+
]
|
30
|
+
]
|
31
|
+
repo: NotRequired[str]
|
32
|
+
events: NotRequired[list[OSVEvent]]
|
33
|
+
|
34
|
+
class OSVPackage(TypedDict):
|
35
|
+
name: NotRequired[str]
|
36
|
+
ecosystem: NotRequired[str]
|
37
|
+
purl: NotRequired[str]
|
38
|
+
|
39
|
+
class OSVCredit(TypedDict):
|
40
|
+
name: NotRequired[str]
|
41
|
+
contact: NotRequired[list[str]]
|
42
|
+
type: NotRequired[
|
43
|
+
Literal[
|
44
|
+
"UNSPECIFIED",
|
45
|
+
"OTHER",
|
46
|
+
"FINDER",
|
47
|
+
"REPORTER",
|
48
|
+
"ANALYST",
|
49
|
+
"COORDINATOR",
|
50
|
+
"REMEDIATION_DEVELOPER",
|
51
|
+
"REMEDIATION_REVIEWER",
|
52
|
+
"REMEDIATION_VERIFIER",
|
53
|
+
"TOOL",
|
54
|
+
"SPONSOR",
|
55
|
+
]
|
56
|
+
]
|
57
|
+
|
58
|
+
class OSVSeverity(TypedDict):
|
59
|
+
type: NotRequired[
|
60
|
+
Literal[
|
61
|
+
"UNSPECIFIED",
|
62
|
+
"CVSS_V4",
|
63
|
+
"CVSS_V3",
|
64
|
+
"CVSS_V2",
|
65
|
+
]
|
66
|
+
]
|
67
|
+
score: NotRequired[str]
|
68
|
+
|
69
|
+
class OSVReference(TypedDict):
|
70
|
+
type: NotRequired[
|
71
|
+
Literal[
|
72
|
+
"NONE",
|
73
|
+
"WEB",
|
74
|
+
"ADVISORY",
|
75
|
+
"REPORT",
|
76
|
+
"FIX",
|
77
|
+
"PACKAGE",
|
78
|
+
"ARTICLE",
|
79
|
+
"EVIDENCE",
|
80
|
+
]
|
81
|
+
]
|
82
|
+
url: NotRequired[str]
|
83
|
+
|
84
|
+
class OSVAffected(TypedDict):
|
85
|
+
package: NotRequired[OSVPackage]
|
86
|
+
ranges: NotRequired[list[OSVRange]]
|
87
|
+
versions: NotRequired[list[str]]
|
88
|
+
ecosystemSpecific: NotRequired[dict[str, Any]]
|
89
|
+
databaseSpecific: NotRequired[dict[str, Any]]
|
90
|
+
severity: NotRequired[list[OSVSeverity]]
|
91
|
+
|
92
|
+
class OSVVulnerability(TypedDict):
|
93
|
+
id: str
|
94
|
+
summary: str
|
95
|
+
schemaVersion: NotRequired[str]
|
96
|
+
published: NotRequired[str]
|
97
|
+
modified: NotRequired[str]
|
98
|
+
withdrawn: NotRequired[str]
|
99
|
+
aliases: NotRequired[list[str]]
|
100
|
+
related: NotRequired[list[str]]
|
101
|
+
details: NotRequired[str]
|
102
|
+
affected: NotRequired[list[OSVAffected]]
|
103
|
+
references: NotRequired[list[OSVReference]]
|
104
|
+
databaseSpecific: NotRequired[dict[str, Any]]
|
105
|
+
severity: NotRequired[list[OSVSeverity]]
|
106
|
+
credits: NotRequired[list[OSVCredit]]
|
107
|
+
|
108
|
+
class V1VulnerabilityList(TypedDict):
|
109
|
+
vulns: NotRequired[list[OSVVulnerability]]
|
110
|
+
nextPageToken: NotRequired[str]
|
111
|
+
|
112
|
+
class V1Query(TypedDict):
|
113
|
+
commit: NotRequired[str]
|
114
|
+
version: NotRequired[str]
|
115
|
+
package: NotRequired[OSVPackage]
|
116
|
+
page_token: NotRequired[str]
|
117
|
+
|
118
|
+
#########################################
|
119
|
+
|
120
|
+
class V1Batchquery(TypedDict):
|
121
|
+
queries: list[V1Query]
|
122
|
+
|
123
|
+
class QueryBatchResult(TypedDict):
|
124
|
+
vulns: list[dict[Literal["id", "modified"], str]]
|
125
|
+
next_page_token: NotRequired[str]
|
126
|
+
|
127
|
+
class QueryBatchResponse(TypedDict):
|
128
|
+
results: list[QueryBatchResult]
|
129
|
+
|
130
|
+
|
131
|
+
logger = logging.getLogger(__name__)
|
132
|
+
|
133
|
+
|
134
|
+
@dataclass
|
135
|
+
class OSVClientV1(BaseClient):
|
136
|
+
base_url: str = "https://api.osv.dev"
|
137
|
+
|
138
|
+
async def query(self, query: V1Query) -> V1VulnerabilityList:
|
139
|
+
"""
|
140
|
+
Lists vulnerabilities for given package and version. May also be queried by commit hash.
|
141
|
+
|
142
|
+
POST /v1/query
|
143
|
+
"""
|
144
|
+
return await self._requests(method="POST", url="/v1/query", json=query) # type:ignore[return-value]
|
145
|
+
|
146
|
+
async def querybatch(self, query: V1Batchquery) -> QueryBatchResponse:
|
147
|
+
"""
|
148
|
+
Query for multiple packages (by either package and version or git commit hash) at once. Returns vulnerability ids and modified field only. The response ordering will be guaranteed to match the input.
|
149
|
+
|
150
|
+
POST /v1/querybatc
|
151
|
+
""" # noqa: E501
|
152
|
+
return await self._requests(method="POST", url="/v1/querybatch", json=query) # type:ignore[return-value]
|
153
|
+
|
154
|
+
async def get_vuln(self, vuln_id: str) -> OSVVulnerability:
|
155
|
+
"""
|
156
|
+
Returns vulnerability information for a given vulnerability id.
|
157
|
+
|
158
|
+
GET /v1/vulns/{id}
|
159
|
+
"""
|
160
|
+
return await self._requests(method="GET", url=f"/v1/vulns/{self.url_escape(vuln_id)}") # type:ignore[return-value]
|
161
|
+
|
162
|
+
# async def import_findings(self) -> Incomplete:
|
163
|
+
# """
|
164
|
+
# Something like this:
|
165
|
+
# """
|
166
|
+
# return await self._requests(method="GET", url="/v1experimental/importfindings")
|
167
|
+
|
168
|
+
# async def determine_version(self) -> Incomplete:
|
169
|
+
# """
|
170
|
+
# Something like this:
|
171
|
+
# """
|
172
|
+
# return await self._requests(method="POST", url="/v1experimental/determineversion")
|
173
|
+
|
174
|
+
|
175
|
+
if __name__ == "__main__":
|
176
|
+
logging.basicConfig(level=logging.INFO)
|
177
|
+
client = OSVClientV1()
|
178
|
+
import asyncio
|
179
|
+
import json
|
180
|
+
|
181
|
+
query: V1Query = {
|
182
|
+
"package": {"name": "jinja2", "ecosystem": "PyPI"},
|
183
|
+
"version": "2.4.1",
|
184
|
+
}
|
185
|
+
loop = asyncio.get_event_loop()
|
186
|
+
a = client.query(query)
|
187
|
+
# a = client.get_vuln("GHSA-3mc7-4q67-w48m")
|
188
|
+
# # {"name": "org.yaml:snakeyaml", "version": "1.19", "system": "MAVEN"}
|
189
|
+
# a = client.query(
|
190
|
+
# {"package": {"name": "org.yaml:snakeyaml", "ecosystem": "MAVEN"}, "version": "1.19"}
|
191
|
+
# )
|
192
|
+
a = client.query({"package": {"purl": "pkg:maven/org.yaml/snakeyaml@1.19"}})
|
193
|
+
|
194
|
+
result = loop.run_until_complete(a)
|
195
|
+
print(json.dumps(result)) # For demonstration purposes, print the result
|
@@ -24,6 +24,9 @@ class HashType(str, Enum):
|
|
24
24
|
SHA256 = "SHA256"
|
25
25
|
SHA512 = "SHA512"
|
26
26
|
|
27
|
+
def __str__(self) -> str:
|
28
|
+
return self.value
|
29
|
+
|
27
30
|
|
28
31
|
class System(str, Enum):
|
29
32
|
GO = "GO"
|
@@ -34,6 +37,9 @@ class System(str, Enum):
|
|
34
37
|
PYPI = "PYPI"
|
35
38
|
NUGET = "NUGET"
|
36
39
|
|
40
|
+
def __str__(self) -> str:
|
41
|
+
return self.value
|
42
|
+
|
37
43
|
|
38
44
|
def url_escape(string: str) -> str:
|
39
45
|
return quote(string, safe="")
|
@@ -41,7 +47,7 @@ def url_escape(string: str) -> str:
|
|
41
47
|
|
42
48
|
@dataclass
|
43
49
|
class DepsDevClientV3:
|
44
|
-
client: httpx.AsyncClient = field(init=False)
|
50
|
+
client: httpx.AsyncClient = field(init=False, repr=False)
|
45
51
|
timeout: float = 5.0
|
46
52
|
base_url: str = "https://api.deps.dev"
|
47
53
|
|
@@ -55,6 +61,7 @@ class DepsDevClientV3:
|
|
55
61
|
params: QueryParamTypes | None = None,
|
56
62
|
json: object | None = None,
|
57
63
|
) -> Incomplete:
|
64
|
+
logger.info(locals())
|
58
65
|
response = await self.client.request(method=method, url=url, params=params, json=json)
|
59
66
|
if not response.is_success:
|
60
67
|
logger.error(
|
File without changes
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from depsdev.v3 import DepsDevClientV3
|
4
|
+
from depsdev.v3 import System
|
5
|
+
|
6
|
+
|
7
|
+
@pytest.mark.asyncio
|
8
|
+
async def test_all() -> None:
|
9
|
+
client = DepsDevClientV3()
|
10
|
+
system = System.NPM
|
11
|
+
name = "@colors/colors"
|
12
|
+
version = "1.5.0"
|
13
|
+
project_id = "github.com/facebook/react"
|
14
|
+
advisory_id = "GHSA-2qrg-x229-3v8q"
|
15
|
+
print(await client.get_package(system, name))
|
16
|
+
print(await client.get_version(system, name, version))
|
17
|
+
print(await client.get_requirements(system, name, version))
|
18
|
+
print(await client.get_dependencies(system, name, version))
|
19
|
+
print(await client.get_project(project_id))
|
20
|
+
print(await client.get_project_package_versions(project_id))
|
21
|
+
print(await client.get_advisory(advisory_id))
|
22
|
+
print(
|
23
|
+
await client.query(
|
24
|
+
system=System.NPM,
|
25
|
+
name="react",
|
26
|
+
version="18.2.0",
|
27
|
+
)
|
28
|
+
)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from depsdev.v3 import System
|
4
|
+
from depsdev.v3alpha import DepsDevClientV3Alpha
|
5
|
+
|
6
|
+
|
7
|
+
@pytest.mark.asyncio
|
8
|
+
async def test_all() -> None:
|
9
|
+
client = DepsDevClientV3Alpha()
|
10
|
+
system = System.NPM
|
11
|
+
name = "@colors/colors"
|
12
|
+
version = "1.5.0"
|
13
|
+
project_id = "github.com/facebook/react"
|
14
|
+
advisory_id = "GHSA-2qrg-x229-3v8q"
|
15
|
+
print(await client.get_package(system, name))
|
16
|
+
print(await client.get_version(system, name, version))
|
17
|
+
print(await client.get_requirements(system, name, version))
|
18
|
+
print(await client.get_dependencies(system, name, version))
|
19
|
+
print(await client.get_project(project_id))
|
20
|
+
print(await client.get_project_package_versions(project_id))
|
21
|
+
print(await client.get_advisory(advisory_id))
|
22
|
+
|
23
|
+
print(
|
24
|
+
await client.query(
|
25
|
+
system=System.NPM,
|
26
|
+
name="react",
|
27
|
+
version="18.2.0",
|
28
|
+
)
|
29
|
+
)
|
30
|
+
|
31
|
+
print(
|
32
|
+
await client.get_version_batch(
|
33
|
+
[
|
34
|
+
{"system": "NPM", "name": "@colors/colors", "version": "1.5.0"},
|
35
|
+
{"system": "NUGET", "name": "castle.core", "version": "5.1.1"},
|
36
|
+
]
|
37
|
+
)
|
38
|
+
)
|
39
|
+
print(await client.get_dependents(system, name, version))
|
40
|
+
# print(await client.get_capabilities(system, name, version))
|
41
|
+
print(
|
42
|
+
await client.get_project_batch(["github.com/facebook/react", "github.com/angular/angular"])
|
43
|
+
)
|
44
|
+
|
45
|
+
purl1 = "pkg:npm/@colors/colors"
|
46
|
+
purl2 = "pkg:npm/@colors/colors@1.5.0"
|
47
|
+
print(await client.get_similarly_named_packages(system, name))
|
48
|
+
print(await client.purl_lookup(purl1))
|
49
|
+
print(await client.purl_lookup_batch([purl2]))
|
50
|
+
# print(await client.query_container_images(""))
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|