osidb-mcp 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.
- osidb_mcp-0.1.0/.gitignore +8 -0
- osidb_mcp-0.1.0/CHANGELOG.md +5 -0
- osidb_mcp-0.1.0/LICENSE +21 -0
- osidb_mcp-0.1.0/PKG-INFO +109 -0
- osidb_mcp-0.1.0/README.md +82 -0
- osidb_mcp-0.1.0/pyproject.toml +45 -0
- osidb_mcp-0.1.0/src/osidb_mcp/__init__.py +3 -0
- osidb_mcp-0.1.0/src/osidb_mcp/__main__.py +37 -0
- osidb_mcp-0.1.0/src/osidb_mcp/config.py +80 -0
- osidb_mcp-0.1.0/src/osidb_mcp/errors.py +20 -0
- osidb_mcp-0.1.0/src/osidb_mcp/query_filters.py +69 -0
- osidb_mcp-0.1.0/src/osidb_mcp/serialize.py +49 -0
- osidb_mcp-0.1.0/src/osidb_mcp/server.py +89 -0
- osidb_mcp-0.1.0/src/osidb_mcp/session_holder.py +42 -0
- osidb_mcp-0.1.0/src/osidb_mcp/tools_read.py +622 -0
osidb_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 osidb-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
osidb_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osidb-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Model Context Protocol (MCP) server for OSIDB using osidb-bindings
|
|
5
|
+
Project-URL: Homepage, https://github.com/RedHatProductSecurity/osidb
|
|
6
|
+
Project-URL: Repository, https://github.com/RedHatProductSecurity/osidb-bindings
|
|
7
|
+
Author: osidb-mcp contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cve,kerberos,mcp,osidb,security
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: mcp<2,>=1.0
|
|
22
|
+
Requires-Dist: osidb-bindings<6,>=5.10.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pip-audit>=2.7; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# osidb-mcp
|
|
29
|
+
|
|
30
|
+
Python [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for [OSIDB](https://github.com/RedHatProductSecurity/osidb), built on [`osidb-bindings`](https://github.com/RedHatProductSecurity/osidb-bindings) from PyPI. Use it from Cursor, Claude Desktop, or any MCP client over **stdio**.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pipx install osidb-mcp
|
|
36
|
+
# or
|
|
37
|
+
pip install osidb-mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration (environment)
|
|
41
|
+
|
|
42
|
+
| Variable | Required | Description |
|
|
43
|
+
|----------|----------|-------------|
|
|
44
|
+
| `OSIDB_BASE_URL` | yes | OSIDB root URL, e.g. `https://osidb.example.com` |
|
|
45
|
+
| `OSIDB_AUTH` | no | `kerberos` (default) or `basic` |
|
|
46
|
+
| `OSIDB_USERNAME` / `OSIDB_PASSWORD` | for `basic` | Basic auth for token obtain |
|
|
47
|
+
| `OSIDB_VERIFY_SSL` | no | `true` (default) or `false` (prefer `REQUESTS_CA_BUNDLE` for custom CAs) |
|
|
48
|
+
| `OSIDB_USER_AGENT` | no | Optional extra User-Agent suffix |
|
|
49
|
+
| `OSIDB_MCP_ACCESS_MODE` | no | `readonly` (default) or `readwrite` (mutations reserved for a future release) |
|
|
50
|
+
|
|
51
|
+
Kerberos: the process must have a valid ticket (`kinit`) for the OSIDB HTTP service.
|
|
52
|
+
|
|
53
|
+
Optional keys forwarded by bindings: `BUGZILLA_API_KEY`, `JIRA_ACCESS_TOKEN`, `JIRA_API_EMAIL`.
|
|
54
|
+
|
|
55
|
+
## Cursor / Claude MCP snippet
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"osidb": {
|
|
61
|
+
"command": "osidb-mcp",
|
|
62
|
+
"env": {
|
|
63
|
+
"OSIDB_BASE_URL": "https://your-internal-osidb",
|
|
64
|
+
"OSIDB_AUTH": "kerberos",
|
|
65
|
+
"OSIDB_VERIFY_SSL": "true",
|
|
66
|
+
"OSIDB_MCP_ACCESS_MODE": "readonly"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Tools (read-only)
|
|
74
|
+
|
|
75
|
+
- `osidb_status` — health / version style status
|
|
76
|
+
- `osidb_whoami` — `/osidb/whoami`
|
|
77
|
+
- `flaw_get` — single flaw with optional `include_fields` / `exclude_fields`
|
|
78
|
+
- `flaws_list` / `flaws_count` — filter by `components_in`, `affects_ps_module_in`, `workflow_state_in`, `impact` / `impact_in`, `owner_isempty`, embargo flags, ISO8601 `changed_after` / `changed_before`, plus allowlisted `extra_query` keys from the OSIDB OpenAPI
|
|
79
|
+
- `flaws_search` — full-text `search`
|
|
80
|
+
- `affects_list` — affect-centric rows with `ps_module` / `ps_component` / `ps_update_stream` and `flaw__*` filters
|
|
81
|
+
- `trackers_list` — tracker filings with CVE / module / component filters
|
|
82
|
+
- `flaw_comments_list`, `flaw_references_list`, `flaw_cvss_scores_list`
|
|
83
|
+
|
|
84
|
+
`limit` is capped at **100** per request.
|
|
85
|
+
|
|
86
|
+
## Analyst examples
|
|
87
|
+
|
|
88
|
+
- **Critical open flaws touching `httpd`:** `flaws_list` with `impact="CRITICAL"`, `workflow_state_in` set to the non-terminal states your instance uses, and `components_in=["httpd"]` or `affects_ps_component="httpd"` depending on how data is modeled.
|
|
89
|
+
- **Unowned important CVEs for a RHEL major:** `flaws_list` with `owner_isempty=true`, `impact_in=["IMPORTANT"]`, and `affects_ps_module_in` / `affects_ps_update_stream_in` set to the **exact** PS strings your OSIDB uses for that major (confirm in your internal docs).
|
|
90
|
+
|
|
91
|
+
## Security
|
|
92
|
+
|
|
93
|
+
- Outputs may include **embargoed** content; treat transcripts and logs according to your data classification policy.
|
|
94
|
+
- Prefer `readonly` (default). `readwrite` does not enable mutations yet but is reserved for explicit future write tools.
|
|
95
|
+
- Never commit `OSIDB_PASSWORD`; use IDE env or secret stores.
|
|
96
|
+
|
|
97
|
+
## Development
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv venv .venv && source .venv/bin/activate
|
|
101
|
+
uv pip install -e ".[dev]"
|
|
102
|
+
python -m osidb_mcp
|
|
103
|
+
pytest
|
|
104
|
+
pip-audit
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# osidb-mcp
|
|
2
|
+
|
|
3
|
+
Python [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for [OSIDB](https://github.com/RedHatProductSecurity/osidb), built on [`osidb-bindings`](https://github.com/RedHatProductSecurity/osidb-bindings) from PyPI. Use it from Cursor, Claude Desktop, or any MCP client over **stdio**.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pipx install osidb-mcp
|
|
9
|
+
# or
|
|
10
|
+
pip install osidb-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration (environment)
|
|
14
|
+
|
|
15
|
+
| Variable | Required | Description |
|
|
16
|
+
|----------|----------|-------------|
|
|
17
|
+
| `OSIDB_BASE_URL` | yes | OSIDB root URL, e.g. `https://osidb.example.com` |
|
|
18
|
+
| `OSIDB_AUTH` | no | `kerberos` (default) or `basic` |
|
|
19
|
+
| `OSIDB_USERNAME` / `OSIDB_PASSWORD` | for `basic` | Basic auth for token obtain |
|
|
20
|
+
| `OSIDB_VERIFY_SSL` | no | `true` (default) or `false` (prefer `REQUESTS_CA_BUNDLE` for custom CAs) |
|
|
21
|
+
| `OSIDB_USER_AGENT` | no | Optional extra User-Agent suffix |
|
|
22
|
+
| `OSIDB_MCP_ACCESS_MODE` | no | `readonly` (default) or `readwrite` (mutations reserved for a future release) |
|
|
23
|
+
|
|
24
|
+
Kerberos: the process must have a valid ticket (`kinit`) for the OSIDB HTTP service.
|
|
25
|
+
|
|
26
|
+
Optional keys forwarded by bindings: `BUGZILLA_API_KEY`, `JIRA_ACCESS_TOKEN`, `JIRA_API_EMAIL`.
|
|
27
|
+
|
|
28
|
+
## Cursor / Claude MCP snippet
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"osidb": {
|
|
34
|
+
"command": "osidb-mcp",
|
|
35
|
+
"env": {
|
|
36
|
+
"OSIDB_BASE_URL": "https://your-internal-osidb",
|
|
37
|
+
"OSIDB_AUTH": "kerberos",
|
|
38
|
+
"OSIDB_VERIFY_SSL": "true",
|
|
39
|
+
"OSIDB_MCP_ACCESS_MODE": "readonly"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tools (read-only)
|
|
47
|
+
|
|
48
|
+
- `osidb_status` — health / version style status
|
|
49
|
+
- `osidb_whoami` — `/osidb/whoami`
|
|
50
|
+
- `flaw_get` — single flaw with optional `include_fields` / `exclude_fields`
|
|
51
|
+
- `flaws_list` / `flaws_count` — filter by `components_in`, `affects_ps_module_in`, `workflow_state_in`, `impact` / `impact_in`, `owner_isempty`, embargo flags, ISO8601 `changed_after` / `changed_before`, plus allowlisted `extra_query` keys from the OSIDB OpenAPI
|
|
52
|
+
- `flaws_search` — full-text `search`
|
|
53
|
+
- `affects_list` — affect-centric rows with `ps_module` / `ps_component` / `ps_update_stream` and `flaw__*` filters
|
|
54
|
+
- `trackers_list` — tracker filings with CVE / module / component filters
|
|
55
|
+
- `flaw_comments_list`, `flaw_references_list`, `flaw_cvss_scores_list`
|
|
56
|
+
|
|
57
|
+
`limit` is capped at **100** per request.
|
|
58
|
+
|
|
59
|
+
## Analyst examples
|
|
60
|
+
|
|
61
|
+
- **Critical open flaws touching `httpd`:** `flaws_list` with `impact="CRITICAL"`, `workflow_state_in` set to the non-terminal states your instance uses, and `components_in=["httpd"]` or `affects_ps_component="httpd"` depending on how data is modeled.
|
|
62
|
+
- **Unowned important CVEs for a RHEL major:** `flaws_list` with `owner_isempty=true`, `impact_in=["IMPORTANT"]`, and `affects_ps_module_in` / `affects_ps_update_stream_in` set to the **exact** PS strings your OSIDB uses for that major (confirm in your internal docs).
|
|
63
|
+
|
|
64
|
+
## Security
|
|
65
|
+
|
|
66
|
+
- Outputs may include **embargoed** content; treat transcripts and logs according to your data classification policy.
|
|
67
|
+
- Prefer `readonly` (default). `readwrite` does not enable mutations yet but is reserved for explicit future write tools.
|
|
68
|
+
- Never commit `OSIDB_PASSWORD`; use IDE env or secret stores.
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv venv .venv && source .venv/bin/activate
|
|
74
|
+
uv pip install -e ".[dev]"
|
|
75
|
+
python -m osidb_mcp
|
|
76
|
+
pytest
|
|
77
|
+
pip-audit
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "osidb-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Model Context Protocol (MCP) server for OSIDB using osidb-bindings"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "osidb-mcp contributors" }]
|
|
14
|
+
keywords = ["mcp", "osidb", "security", "cve", "kerberos"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Information Technology",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"mcp>=1.0,<2",
|
|
28
|
+
"osidb-bindings>=5.10.0,<6",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = ["pytest>=8.0", "pip-audit>=2.7"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
osidb-mcp = "osidb_mcp.__main__:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/RedHatProductSecurity/osidb"
|
|
39
|
+
Repository = "https://github.com/RedHatProductSecurity/osidb-bindings"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = ["src/osidb_mcp", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Console entrypoint: ``osidb-mcp`` and ``python -m osidb_mcp``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from osidb_mcp.config import AccessMode, load_settings
|
|
9
|
+
from osidb_mcp.server import create_server
|
|
10
|
+
from osidb_mcp.session_holder import init_session
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
logging.basicConfig(
|
|
15
|
+
level=logging.INFO,
|
|
16
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
17
|
+
stream=sys.stderr,
|
|
18
|
+
)
|
|
19
|
+
log = logging.getLogger("osidb_mcp")
|
|
20
|
+
|
|
21
|
+
settings = load_settings()
|
|
22
|
+
init_session(settings)
|
|
23
|
+
|
|
24
|
+
if settings.access_mode == AccessMode.readwrite:
|
|
25
|
+
log.warning(
|
|
26
|
+
"OSIDB_MCP_ACCESS_MODE=readwrite — mutation MCP tools are not implemented yet; "
|
|
27
|
+
"only read tools are registered."
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
log.info("osidb-mcp access mode: readonly")
|
|
31
|
+
|
|
32
|
+
mcp = create_server(settings)
|
|
33
|
+
mcp.run(transport="stdio")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Load configuration from the environment (no secrets in source code)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AccessMode(str, Enum):
|
|
12
|
+
"""Server capability gate: only ``readwrite`` may register mutation tools (future)."""
|
|
13
|
+
|
|
14
|
+
readonly = "readonly"
|
|
15
|
+
readwrite = "readwrite"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
19
|
+
raw = os.environ.get(name)
|
|
20
|
+
if raw is None or raw == "":
|
|
21
|
+
return default
|
|
22
|
+
return raw.strip().lower() in {"1", "true", "yes", "y", "on"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_access_mode() -> AccessMode:
|
|
26
|
+
raw = (os.environ.get("OSIDB_MCP_ACCESS_MODE") or "readonly").strip().lower()
|
|
27
|
+
if raw == "readonly":
|
|
28
|
+
return AccessMode.readonly
|
|
29
|
+
if raw == "readwrite":
|
|
30
|
+
return AccessMode.readwrite
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"OSIDB_MCP_ACCESS_MODE must be 'readonly' or 'readwrite', "
|
|
33
|
+
f"got {raw!r}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class Settings:
|
|
39
|
+
base_url: str
|
|
40
|
+
auth: Literal["kerberos", "basic"]
|
|
41
|
+
username: str | None
|
|
42
|
+
password: str | None
|
|
43
|
+
verify_ssl: bool
|
|
44
|
+
user_agent: str | None
|
|
45
|
+
access_mode: AccessMode
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_settings() -> Settings:
|
|
49
|
+
base = os.environ.get("OSIDB_BASE_URL", "").strip()
|
|
50
|
+
if not base:
|
|
51
|
+
raise ValueError("OSIDB_BASE_URL is required")
|
|
52
|
+
|
|
53
|
+
auth = (os.environ.get("OSIDB_AUTH") or "kerberos").strip().lower()
|
|
54
|
+
if auth not in ("kerberos", "basic"):
|
|
55
|
+
raise ValueError("OSIDB_AUTH must be 'kerberos' or 'basic'")
|
|
56
|
+
|
|
57
|
+
username = os.environ.get("OSIDB_USERNAME")
|
|
58
|
+
password = os.environ.get("OSIDB_PASSWORD")
|
|
59
|
+
if auth == "basic":
|
|
60
|
+
if not username or not password:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"OSIDB_AUTH=basic requires OSIDB_USERNAME and OSIDB_PASSWORD"
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
username = None
|
|
66
|
+
password = None
|
|
67
|
+
|
|
68
|
+
ua = os.environ.get("OSIDB_USER_AGENT")
|
|
69
|
+
if ua == "":
|
|
70
|
+
ua = None
|
|
71
|
+
|
|
72
|
+
return Settings(
|
|
73
|
+
base_url=base,
|
|
74
|
+
auth=auth, # type: ignore[arg-type]
|
|
75
|
+
username=username,
|
|
76
|
+
password=password,
|
|
77
|
+
verify_ssl=_env_bool("OSIDB_VERIFY_SSL", True),
|
|
78
|
+
user_agent=ua,
|
|
79
|
+
access_mode=_parse_access_mode(),
|
|
80
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Format HTTP errors without leaking secrets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def http_error_payload(exc: BaseException) -> dict[str, Any]:
|
|
11
|
+
if isinstance(exc, requests.HTTPError):
|
|
12
|
+
resp = exc.response
|
|
13
|
+
status = resp.status_code if resp is not None else None
|
|
14
|
+
text = (resp.text[:2000] if resp is not None else "") or ""
|
|
15
|
+
return {
|
|
16
|
+
"error": "osidb_http_error",
|
|
17
|
+
"status_code": status,
|
|
18
|
+
"detail": text,
|
|
19
|
+
}
|
|
20
|
+
return {"error": "osidb_error", "detail": str(exc)}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Build kwargs for flaws/affects list with allowlisted ``extra_query``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from osidb_bindings.bindings.python_client.api.osidb import (
|
|
8
|
+
osidb_api_v2_affects_list,
|
|
9
|
+
osidb_api_v2_flaws_list,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
FLAWS_EXTRA_KEYS = frozenset(osidb_api_v2_flaws_list.QUERY_PARAMS.keys())
|
|
13
|
+
AFFECTS_EXTRA_KEYS = frozenset(osidb_api_v2_affects_list.QUERY_PARAMS.keys())
|
|
14
|
+
|
|
15
|
+
EXTRAS_MAX_KEYS = 30
|
|
16
|
+
EXTRAS_MAX_LIST_LEN = 100
|
|
17
|
+
LIST_LIMIT_MAX = 100
|
|
18
|
+
DEFAULT_LIST_LIMIT = 50
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _coerce_extra_value(value: Any) -> Any:
|
|
22
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, list):
|
|
25
|
+
if len(value) > EXTRAS_MAX_LIST_LEN:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"extra_query list values must have at most {EXTRAS_MAX_LIST_LEN} items"
|
|
28
|
+
)
|
|
29
|
+
out = []
|
|
30
|
+
for item in value:
|
|
31
|
+
if not isinstance(item, (str, int, float, bool)):
|
|
32
|
+
raise ValueError("extra_query list items must be primitives")
|
|
33
|
+
out.append(item)
|
|
34
|
+
return out
|
|
35
|
+
raise ValueError("extra_query values must be primitives or lists of primitives")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def merge_extra_query(
|
|
39
|
+
base: dict[str, Any],
|
|
40
|
+
extra: dict[str, Any] | None,
|
|
41
|
+
*,
|
|
42
|
+
allowlist: frozenset[str],
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
if not extra:
|
|
45
|
+
return base
|
|
46
|
+
if len(extra) > EXTRAS_MAX_KEYS:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"extra_query may contain at most {EXTRAS_MAX_KEYS} keys"
|
|
49
|
+
)
|
|
50
|
+
merged = dict(base)
|
|
51
|
+
for key, raw in extra.items():
|
|
52
|
+
if key not in allowlist:
|
|
53
|
+
raise ValueError(f"extra_query key not allowed: {key!r}")
|
|
54
|
+
merged[key] = _coerce_extra_value(raw)
|
|
55
|
+
return merged
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clamp_limit(limit: int | None) -> int:
|
|
59
|
+
if limit is None:
|
|
60
|
+
return DEFAULT_LIST_LIMIT
|
|
61
|
+
if limit < 1:
|
|
62
|
+
return 1
|
|
63
|
+
return min(limit, LIST_LIMIT_MAX)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def clamp_offset(offset: int | None) -> int:
|
|
67
|
+
if offset is None or offset < 0:
|
|
68
|
+
return 0
|
|
69
|
+
return offset
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""JSON-friendly serialization for bindings models and responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def to_jsonable(obj: Any) -> Any:
|
|
12
|
+
if obj is None:
|
|
13
|
+
return None
|
|
14
|
+
if isinstance(obj, (str, int, float, bool)):
|
|
15
|
+
return obj
|
|
16
|
+
if isinstance(obj, UUID):
|
|
17
|
+
return str(obj)
|
|
18
|
+
if isinstance(obj, Enum):
|
|
19
|
+
return obj.value
|
|
20
|
+
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
21
|
+
return obj.isoformat()
|
|
22
|
+
if isinstance(obj, dict):
|
|
23
|
+
return {str(k): to_jsonable(v) for k, v in obj.items()}
|
|
24
|
+
if isinstance(obj, (list, tuple)):
|
|
25
|
+
return [to_jsonable(x) for x in obj]
|
|
26
|
+
if hasattr(obj, "to_dict"):
|
|
27
|
+
return to_jsonable(obj.to_dict())
|
|
28
|
+
return str(obj)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def paginated_summary(
|
|
32
|
+
response: Any,
|
|
33
|
+
*,
|
|
34
|
+
limit: int,
|
|
35
|
+
offset: int,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""Common pagination envelope for list endpoints."""
|
|
38
|
+
results = getattr(response, "results", ()) or ()
|
|
39
|
+
out: dict[str, Any] = {
|
|
40
|
+
"count": getattr(response, "count", None),
|
|
41
|
+
"limit": limit,
|
|
42
|
+
"offset": offset,
|
|
43
|
+
"results": [to_jsonable(r) for r in results],
|
|
44
|
+
}
|
|
45
|
+
next_url = getattr(response, "next_", None)
|
|
46
|
+
if next_url:
|
|
47
|
+
out["next"] = next_url
|
|
48
|
+
out["next_offset"] = offset + len(results)
|
|
49
|
+
return out
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""FastMCP application wiring (stdio)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
|
|
9
|
+
from osidb_mcp.config import AccessMode, Settings
|
|
10
|
+
from osidb_mcp import tools_read
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_INSTRUCTIONS = """\
|
|
15
|
+
This server exposes read-only OSIDB operations (flaws/CVEs, affects, trackers, comments, references, CVSS) \
|
|
16
|
+
via official osidb-bindings. Responses may include embargoed data depending on your OSIDB account.
|
|
17
|
+
|
|
18
|
+
Use ``flaws_list`` / ``flaws_count`` with ``components_in``, ``affects_ps_component``, ``affects_ps_module_in``, \
|
|
19
|
+
``workflow_state_in``, ``impact`` / ``impact_in``, and ``owner_isempty`` for common triage queries. \
|
|
20
|
+
Use ``affects_list`` when you want rows keyed by affect (``ps_module`` / ``ps_component`` / ``ps_update_stream``) \
|
|
21
|
+
with ``flaw__*`` filters. Workflow state strings are deployment-specific.
|
|
22
|
+
|
|
23
|
+
Access mode is controlled only by the ``OSIDB_MCP_ACCESS_MODE`` environment variable (``readonly`` default; \
|
|
24
|
+
``readwrite`` reserved for future mutation tools).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_server(settings: Settings) -> FastMCP:
|
|
29
|
+
mcp = FastMCP("osidb-mcp", instructions=_INSTRUCTIONS)
|
|
30
|
+
|
|
31
|
+
mcp.tool(name="osidb_status", description="OSIDB API health / status payload.")(
|
|
32
|
+
tools_read.osidb_status
|
|
33
|
+
)
|
|
34
|
+
mcp.tool(
|
|
35
|
+
name="osidb_whoami",
|
|
36
|
+
description="Current authenticated OSIDB user / profile (from /osidb/whoami).",
|
|
37
|
+
)(tools_read.osidb_whoami)
|
|
38
|
+
mcp.tool(
|
|
39
|
+
name="flaw_get",
|
|
40
|
+
description="Retrieve a single flaw by CVE id or flaw id; optional field projection.",
|
|
41
|
+
)(tools_read.flaw_get)
|
|
42
|
+
mcp.tool(
|
|
43
|
+
name="flaws_list",
|
|
44
|
+
description=(
|
|
45
|
+
"List flaws with filters: components, affects (ps_module/ps_component/ps_update_stream), "
|
|
46
|
+
"workflow_state, impact, owner_isempty, embargoed, dates, etc. "
|
|
47
|
+
"Optional ``extra_query`` must use OSIDB v2 list query keys (allowlisted). "
|
|
48
|
+
"limit is capped at 100."
|
|
49
|
+
),
|
|
50
|
+
)(tools_read.flaws_list)
|
|
51
|
+
mcp.tool(
|
|
52
|
+
name="flaws_count",
|
|
53
|
+
description="Count flaws matching the same filters as flaws_list (no result bodies).",
|
|
54
|
+
)(tools_read.flaws_count)
|
|
55
|
+
mcp.tool(
|
|
56
|
+
name="flaws_search",
|
|
57
|
+
description="Full-text search flaws (maps to OSIDB search parameter).",
|
|
58
|
+
)(tools_read.flaws_search)
|
|
59
|
+
mcp.tool(
|
|
60
|
+
name="affects_list",
|
|
61
|
+
description=(
|
|
62
|
+
"List affects with ps_module / ps_component / ps_update_stream and flaw__ filters "
|
|
63
|
+
"(e.g. flaw_workflow_state_in, flaw_impact_in, flaw_components_in)."
|
|
64
|
+
),
|
|
65
|
+
)(tools_read.affects_list)
|
|
66
|
+
mcp.tool(
|
|
67
|
+
name="trackers_list",
|
|
68
|
+
description="List trackers (filings) with optional CVE / ps_module / ps_component filters.",
|
|
69
|
+
)(tools_read.trackers_list)
|
|
70
|
+
mcp.tool(
|
|
71
|
+
name="flaw_comments_list",
|
|
72
|
+
description="Paginated comments for a flaw id.",
|
|
73
|
+
)(tools_read.flaw_comments_list)
|
|
74
|
+
mcp.tool(
|
|
75
|
+
name="flaw_references_list",
|
|
76
|
+
description="Paginated external references for a flaw id.",
|
|
77
|
+
)(tools_read.flaw_references_list)
|
|
78
|
+
mcp.tool(
|
|
79
|
+
name="flaw_cvss_scores_list",
|
|
80
|
+
description="Paginated CVSS score rows for a flaw id.",
|
|
81
|
+
)(tools_read.flaw_cvss_scores_list)
|
|
82
|
+
|
|
83
|
+
if settings.access_mode == AccessMode.readwrite:
|
|
84
|
+
logger.warning(
|
|
85
|
+
"readwrite mode requested: mutation tools are not implemented in this release; "
|
|
86
|
+
"only read tools are registered."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return mcp
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Lazy OSIDB session (JWT refresh handled by osidb-bindings)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import osidb_bindings
|
|
6
|
+
from osidb_bindings.session import Session
|
|
7
|
+
|
|
8
|
+
from osidb_mcp.config import Settings
|
|
9
|
+
|
|
10
|
+
_session: Session | None = None
|
|
11
|
+
_settings: Settings | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def init_session(settings: Settings) -> None:
|
|
15
|
+
global _session, _settings
|
|
16
|
+
_settings = settings
|
|
17
|
+
if settings.auth == "basic":
|
|
18
|
+
_session = osidb_bindings.new_session(
|
|
19
|
+
osidb_server_uri=settings.base_url,
|
|
20
|
+
username=settings.username,
|
|
21
|
+
password=settings.password,
|
|
22
|
+
verify_ssl=settings.verify_ssl,
|
|
23
|
+
user_agent=settings.user_agent,
|
|
24
|
+
)
|
|
25
|
+
else:
|
|
26
|
+
_session = osidb_bindings.new_session(
|
|
27
|
+
osidb_server_uri=settings.base_url,
|
|
28
|
+
verify_ssl=settings.verify_ssl,
|
|
29
|
+
user_agent=settings.user_agent,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_session() -> Session:
|
|
34
|
+
if _session is None:
|
|
35
|
+
raise RuntimeError("Session not initialized")
|
|
36
|
+
return _session
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def current_settings() -> Settings:
|
|
40
|
+
if _settings is None:
|
|
41
|
+
raise RuntimeError("Session not initialized")
|
|
42
|
+
return _settings
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""MCP tool implementations (read-only OSIDB operations)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import importlib
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from osidb_bindings.bindings.python_client.api.osidb import osidb_whoami_retrieve
|
|
11
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_affects_list_flaw_impact import (
|
|
12
|
+
OsidbApiV2AffectsListFlawImpact,
|
|
13
|
+
)
|
|
14
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_affects_list_flaw_impact_in_item import (
|
|
15
|
+
OsidbApiV2AffectsListFlawImpactInItem,
|
|
16
|
+
)
|
|
17
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_affects_list_flaw_workflow_state_item import (
|
|
18
|
+
OsidbApiV2AffectsListFlawWorkflowStateItem,
|
|
19
|
+
)
|
|
20
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_flaws_list_impact import (
|
|
21
|
+
OsidbApiV2FlawsListImpact,
|
|
22
|
+
)
|
|
23
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_flaws_list_impact_in_item import (
|
|
24
|
+
OsidbApiV2FlawsListImpactInItem,
|
|
25
|
+
)
|
|
26
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_flaws_list_major_incident_state import (
|
|
27
|
+
OsidbApiV2FlawsListMajorIncidentState,
|
|
28
|
+
)
|
|
29
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_flaws_list_source import (
|
|
30
|
+
OsidbApiV2FlawsListSource,
|
|
31
|
+
)
|
|
32
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_flaws_list_workflow_state_item import (
|
|
33
|
+
OsidbApiV2FlawsListWorkflowStateItem,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from osidb_mcp.errors import http_error_payload
|
|
37
|
+
from osidb_mcp.query_filters import (
|
|
38
|
+
AFFECTS_EXTRA_KEYS,
|
|
39
|
+
DEFAULT_LIST_LIMIT,
|
|
40
|
+
FLAWS_EXTRA_KEYS,
|
|
41
|
+
clamp_limit,
|
|
42
|
+
clamp_offset,
|
|
43
|
+
merge_extra_query,
|
|
44
|
+
)
|
|
45
|
+
from osidb_mcp.serialize import paginated_summary, to_jsonable
|
|
46
|
+
from osidb_mcp.session_holder import get_session
|
|
47
|
+
|
|
48
|
+
_trackers_list = importlib.import_module(
|
|
49
|
+
"osidb_bindings.bindings.python_client.api.osidb.osidb_api_v2_trackers_list"
|
|
50
|
+
)
|
|
51
|
+
TRACKERS_EXTRA_KEYS = frozenset(_trackers_list.QUERY_PARAMS.keys())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_dt(value: str | None) -> datetime.datetime | None:
|
|
55
|
+
if not value:
|
|
56
|
+
return None
|
|
57
|
+
s = value.strip()
|
|
58
|
+
if s.endswith("Z"):
|
|
59
|
+
s = s[:-1] + "+00:00:00"
|
|
60
|
+
return datetime.datetime.fromisoformat(s)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _impact_in(values: list[str] | None) -> list[OsidbApiV2FlawsListImpactInItem] | None:
|
|
64
|
+
if not values:
|
|
65
|
+
return None
|
|
66
|
+
return [OsidbApiV2FlawsListImpactInItem(v) for v in values]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _workflow_in(
|
|
70
|
+
values: list[str] | None,
|
|
71
|
+
) -> list[OsidbApiV2FlawsListWorkflowStateItem] | None:
|
|
72
|
+
if not values:
|
|
73
|
+
return None
|
|
74
|
+
return [OsidbApiV2FlawsListWorkflowStateItem(v) for v in values]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def osidb_status() -> dict[str, Any]:
|
|
78
|
+
try:
|
|
79
|
+
st = get_session().status()
|
|
80
|
+
return {"ok": True, "status": to_jsonable(st)}
|
|
81
|
+
except requests.RequestException as e:
|
|
82
|
+
return {"ok": False, **http_error_payload(e)}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def osidb_whoami() -> dict[str, Any]:
|
|
86
|
+
try:
|
|
87
|
+
s = get_session()
|
|
88
|
+
r = osidb_whoami_retrieve.sync_detailed(
|
|
89
|
+
client=s.get_client_with_new_access_token()
|
|
90
|
+
)
|
|
91
|
+
if r.parsed is None:
|
|
92
|
+
return {
|
|
93
|
+
"ok": False,
|
|
94
|
+
"error": "whoami_empty",
|
|
95
|
+
"status_code": int(r.status_code),
|
|
96
|
+
}
|
|
97
|
+
return {"ok": True, "whoami": to_jsonable(r.parsed.to_dict())}
|
|
98
|
+
except requests.RequestException as e:
|
|
99
|
+
return {"ok": False, **http_error_payload(e)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def flaw_get(
|
|
103
|
+
flaw_id: str,
|
|
104
|
+
include_fields: list[str] | None = None,
|
|
105
|
+
exclude_fields: list[str] | None = None,
|
|
106
|
+
api_version: str | None = None,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
try:
|
|
109
|
+
kw: dict[str, Any] = {}
|
|
110
|
+
if include_fields:
|
|
111
|
+
kw["include_fields"] = include_fields
|
|
112
|
+
if exclude_fields:
|
|
113
|
+
kw["exclude_fields"] = exclude_fields
|
|
114
|
+
flaw = get_session().flaws.retrieve(
|
|
115
|
+
flaw_id, api_version=api_version, **kw
|
|
116
|
+
)
|
|
117
|
+
return {"ok": True, "flaw": to_jsonable(flaw)}
|
|
118
|
+
except requests.RequestException as e:
|
|
119
|
+
return {"ok": False, **http_error_payload(e)}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build_flaws_kwargs(
|
|
123
|
+
*,
|
|
124
|
+
search: str | None,
|
|
125
|
+
embargoed: bool | None,
|
|
126
|
+
components: list[str] | None,
|
|
127
|
+
components_in: list[str] | None,
|
|
128
|
+
affects_ps_module: str | None,
|
|
129
|
+
affects_ps_module_in: list[str] | None,
|
|
130
|
+
affects_ps_component: str | None,
|
|
131
|
+
affects_ps_component_in: list[str] | None,
|
|
132
|
+
affects_ps_update_stream: str | None,
|
|
133
|
+
affects_ps_update_stream_in: list[str] | None,
|
|
134
|
+
workflow_state: list[str] | None,
|
|
135
|
+
workflow_state_in: list[str] | None,
|
|
136
|
+
impact: str | None,
|
|
137
|
+
impact_in: list[str] | None,
|
|
138
|
+
owner: str | None,
|
|
139
|
+
owner_in: list[str] | None,
|
|
140
|
+
owner_isempty: bool | None,
|
|
141
|
+
cve_id_in: list[str] | None,
|
|
142
|
+
changed_after: str | None,
|
|
143
|
+
changed_before: str | None,
|
|
144
|
+
major_incident_state: str | None,
|
|
145
|
+
source: str | None,
|
|
146
|
+
source_in: list[str] | None,
|
|
147
|
+
include_fields: list[str] | None,
|
|
148
|
+
exclude_fields: list[str] | None,
|
|
149
|
+
limit: int,
|
|
150
|
+
offset: int,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
kw: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
153
|
+
if search:
|
|
154
|
+
kw["search"] = search
|
|
155
|
+
if embargoed is not None:
|
|
156
|
+
kw["embargoed"] = embargoed
|
|
157
|
+
if components:
|
|
158
|
+
kw["components"] = components
|
|
159
|
+
if components_in:
|
|
160
|
+
kw["components__in"] = components_in
|
|
161
|
+
if affects_ps_module:
|
|
162
|
+
kw["affects__ps_module"] = affects_ps_module
|
|
163
|
+
if affects_ps_module_in:
|
|
164
|
+
kw["affects__ps_module__in"] = affects_ps_module_in
|
|
165
|
+
if affects_ps_component:
|
|
166
|
+
kw["affects__ps_component"] = affects_ps_component
|
|
167
|
+
if affects_ps_component_in:
|
|
168
|
+
kw["affects__ps_component__in"] = affects_ps_component_in
|
|
169
|
+
if affects_ps_update_stream:
|
|
170
|
+
kw["affects__ps_update_stream"] = affects_ps_update_stream
|
|
171
|
+
if affects_ps_update_stream_in:
|
|
172
|
+
kw["affects__ps_update_stream__in"] = affects_ps_update_stream_in
|
|
173
|
+
if workflow_state:
|
|
174
|
+
kw["workflow_state"] = _workflow_in(workflow_state)
|
|
175
|
+
if workflow_state_in:
|
|
176
|
+
kw["workflow_state__in"] = _workflow_in(workflow_state_in)
|
|
177
|
+
if impact:
|
|
178
|
+
kw["impact"] = OsidbApiV2FlawsListImpact(impact)
|
|
179
|
+
if impact_in:
|
|
180
|
+
kw["impact__in"] = _impact_in(impact_in)
|
|
181
|
+
if owner:
|
|
182
|
+
kw["owner"] = owner
|
|
183
|
+
if owner_in:
|
|
184
|
+
kw["owner__in"] = owner_in
|
|
185
|
+
if owner_isempty is not None:
|
|
186
|
+
kw["owner__isempty"] = owner_isempty
|
|
187
|
+
if cve_id_in:
|
|
188
|
+
kw["cve_id__in"] = cve_id_in
|
|
189
|
+
ca, cb = _parse_dt(changed_after), _parse_dt(changed_before)
|
|
190
|
+
if ca is not None:
|
|
191
|
+
kw["changed_after"] = ca
|
|
192
|
+
if cb is not None:
|
|
193
|
+
kw["changed_before"] = cb
|
|
194
|
+
if major_incident_state:
|
|
195
|
+
kw["major_incident_state"] = OsidbApiV2FlawsListMajorIncidentState(
|
|
196
|
+
major_incident_state
|
|
197
|
+
)
|
|
198
|
+
if source:
|
|
199
|
+
kw["source"] = OsidbApiV2FlawsListSource(source)
|
|
200
|
+
if source_in:
|
|
201
|
+
kw["source__in"] = [OsidbApiV2FlawsListSource(s) for s in source_in]
|
|
202
|
+
if include_fields:
|
|
203
|
+
kw["include_fields"] = include_fields
|
|
204
|
+
if exclude_fields:
|
|
205
|
+
kw["exclude_fields"] = exclude_fields
|
|
206
|
+
return kw
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def flaws_list(
|
|
210
|
+
*,
|
|
211
|
+
search: str | None = None,
|
|
212
|
+
embargoed: bool | None = None,
|
|
213
|
+
components: list[str] | None = None,
|
|
214
|
+
components_in: list[str] | None = None,
|
|
215
|
+
affects_ps_module: str | None = None,
|
|
216
|
+
affects_ps_module_in: list[str] | None = None,
|
|
217
|
+
affects_ps_component: str | None = None,
|
|
218
|
+
affects_ps_component_in: list[str] | None = None,
|
|
219
|
+
affects_ps_update_stream: str | None = None,
|
|
220
|
+
affects_ps_update_stream_in: list[str] | None = None,
|
|
221
|
+
workflow_state: list[str] | None = None,
|
|
222
|
+
workflow_state_in: list[str] | None = None,
|
|
223
|
+
impact: str | None = None,
|
|
224
|
+
impact_in: list[str] | None = None,
|
|
225
|
+
owner: str | None = None,
|
|
226
|
+
owner_in: list[str] | None = None,
|
|
227
|
+
owner_isempty: bool | None = None,
|
|
228
|
+
cve_id_in: list[str] | None = None,
|
|
229
|
+
changed_after: str | None = None,
|
|
230
|
+
changed_before: str | None = None,
|
|
231
|
+
major_incident_state: str | None = None,
|
|
232
|
+
source: str | None = None,
|
|
233
|
+
source_in: list[str] | None = None,
|
|
234
|
+
include_fields: list[str] | None = None,
|
|
235
|
+
exclude_fields: list[str] | None = None,
|
|
236
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
237
|
+
offset: int = 0,
|
|
238
|
+
api_version: str | None = None,
|
|
239
|
+
extra_query: dict[str, Any] | None = None,
|
|
240
|
+
) -> dict[str, Any]:
|
|
241
|
+
lim = clamp_limit(limit)
|
|
242
|
+
off = clamp_offset(offset)
|
|
243
|
+
try:
|
|
244
|
+
base = _build_flaws_kwargs(
|
|
245
|
+
search=search,
|
|
246
|
+
embargoed=embargoed,
|
|
247
|
+
components=components,
|
|
248
|
+
components_in=components_in,
|
|
249
|
+
affects_ps_module=affects_ps_module,
|
|
250
|
+
affects_ps_module_in=affects_ps_module_in,
|
|
251
|
+
affects_ps_component=affects_ps_component,
|
|
252
|
+
affects_ps_component_in=affects_ps_component_in,
|
|
253
|
+
affects_ps_update_stream=affects_ps_update_stream,
|
|
254
|
+
affects_ps_update_stream_in=affects_ps_update_stream_in,
|
|
255
|
+
workflow_state=workflow_state,
|
|
256
|
+
workflow_state_in=workflow_state_in,
|
|
257
|
+
impact=impact,
|
|
258
|
+
impact_in=impact_in,
|
|
259
|
+
owner=owner,
|
|
260
|
+
owner_in=owner_in,
|
|
261
|
+
owner_isempty=owner_isempty,
|
|
262
|
+
cve_id_in=cve_id_in,
|
|
263
|
+
changed_after=changed_after,
|
|
264
|
+
changed_before=changed_before,
|
|
265
|
+
major_incident_state=major_incident_state,
|
|
266
|
+
source=source,
|
|
267
|
+
source_in=source_in,
|
|
268
|
+
include_fields=include_fields,
|
|
269
|
+
exclude_fields=exclude_fields,
|
|
270
|
+
limit=lim,
|
|
271
|
+
offset=off,
|
|
272
|
+
)
|
|
273
|
+
merged = merge_extra_query(base, extra_query, allowlist=FLAWS_EXTRA_KEYS)
|
|
274
|
+
resp = get_session().flaws.retrieve_list(
|
|
275
|
+
api_version=api_version,
|
|
276
|
+
**merged,
|
|
277
|
+
)
|
|
278
|
+
return {
|
|
279
|
+
"ok": True,
|
|
280
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
281
|
+
}
|
|
282
|
+
except (requests.RequestException, ValueError) as e:
|
|
283
|
+
if isinstance(e, ValueError):
|
|
284
|
+
return {"ok": False, "error": "bad_request", "detail": str(e)}
|
|
285
|
+
return {"ok": False, **http_error_payload(e)}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def flaws_count(
|
|
289
|
+
*,
|
|
290
|
+
search: str | None = None,
|
|
291
|
+
embargoed: bool | None = None,
|
|
292
|
+
components: list[str] | None = None,
|
|
293
|
+
components_in: list[str] | None = None,
|
|
294
|
+
affects_ps_module: str | None = None,
|
|
295
|
+
affects_ps_module_in: list[str] | None = None,
|
|
296
|
+
affects_ps_component: str | None = None,
|
|
297
|
+
affects_ps_component_in: list[str] | None = None,
|
|
298
|
+
affects_ps_update_stream: str | None = None,
|
|
299
|
+
affects_ps_update_stream_in: list[str] | None = None,
|
|
300
|
+
workflow_state: list[str] | None = None,
|
|
301
|
+
workflow_state_in: list[str] | None = None,
|
|
302
|
+
impact: str | None = None,
|
|
303
|
+
impact_in: list[str] | None = None,
|
|
304
|
+
owner: str | None = None,
|
|
305
|
+
owner_in: list[str] | None = None,
|
|
306
|
+
owner_isempty: bool | None = None,
|
|
307
|
+
cve_id_in: list[str] | None = None,
|
|
308
|
+
changed_after: str | None = None,
|
|
309
|
+
changed_before: str | None = None,
|
|
310
|
+
major_incident_state: str | None = None,
|
|
311
|
+
source: str | None = None,
|
|
312
|
+
source_in: list[str] | None = None,
|
|
313
|
+
api_version: str | None = None,
|
|
314
|
+
extra_query: dict[str, Any] | None = None,
|
|
315
|
+
) -> dict[str, Any]:
|
|
316
|
+
try:
|
|
317
|
+
base = _build_flaws_kwargs(
|
|
318
|
+
search=search,
|
|
319
|
+
embargoed=embargoed,
|
|
320
|
+
components=components,
|
|
321
|
+
components_in=components_in,
|
|
322
|
+
affects_ps_module=affects_ps_module,
|
|
323
|
+
affects_ps_module_in=affects_ps_module_in,
|
|
324
|
+
affects_ps_component=affects_ps_component,
|
|
325
|
+
affects_ps_component_in=affects_ps_component_in,
|
|
326
|
+
affects_ps_update_stream=affects_ps_update_stream,
|
|
327
|
+
affects_ps_update_stream_in=affects_ps_update_stream_in,
|
|
328
|
+
workflow_state=workflow_state,
|
|
329
|
+
workflow_state_in=workflow_state_in,
|
|
330
|
+
impact=impact,
|
|
331
|
+
impact_in=impact_in,
|
|
332
|
+
owner=owner,
|
|
333
|
+
owner_in=owner_in,
|
|
334
|
+
owner_isempty=owner_isempty,
|
|
335
|
+
cve_id_in=cve_id_in,
|
|
336
|
+
changed_after=changed_after,
|
|
337
|
+
changed_before=changed_before,
|
|
338
|
+
major_incident_state=major_incident_state,
|
|
339
|
+
source=source,
|
|
340
|
+
source_in=source_in,
|
|
341
|
+
include_fields=None,
|
|
342
|
+
exclude_fields=None,
|
|
343
|
+
limit=50,
|
|
344
|
+
offset=0,
|
|
345
|
+
)
|
|
346
|
+
base.pop("limit", None)
|
|
347
|
+
base.pop("offset", None)
|
|
348
|
+
merged = merge_extra_query(base, extra_query, allowlist=FLAWS_EXTRA_KEYS)
|
|
349
|
+
n = get_session().flaws.count(api_version=api_version, **merged)
|
|
350
|
+
return {"ok": True, "count": n}
|
|
351
|
+
except (requests.RequestException, ValueError) as e:
|
|
352
|
+
if isinstance(e, ValueError):
|
|
353
|
+
return {"ok": False, "error": "bad_request", "detail": str(e)}
|
|
354
|
+
return {"ok": False, **http_error_payload(e)}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def flaws_search(
|
|
358
|
+
text: str,
|
|
359
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
360
|
+
api_version: str | None = None,
|
|
361
|
+
) -> dict[str, Any]:
|
|
362
|
+
lim = clamp_limit(limit)
|
|
363
|
+
try:
|
|
364
|
+
resp = get_session().flaws.search(
|
|
365
|
+
text,
|
|
366
|
+
api_version=api_version,
|
|
367
|
+
limit=lim,
|
|
368
|
+
)
|
|
369
|
+
return {
|
|
370
|
+
"ok": True,
|
|
371
|
+
**paginated_summary(resp, limit=lim, offset=0),
|
|
372
|
+
}
|
|
373
|
+
except requests.RequestException as e:
|
|
374
|
+
return {"ok": False, **http_error_payload(e)}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _affects_impact_in(
|
|
378
|
+
values: list[str] | None,
|
|
379
|
+
) -> list[OsidbApiV2AffectsListFlawImpactInItem] | None:
|
|
380
|
+
if not values:
|
|
381
|
+
return None
|
|
382
|
+
return [OsidbApiV2AffectsListFlawImpactInItem(v) for v in values]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _affects_workflow_in(
|
|
386
|
+
values: list[str] | None,
|
|
387
|
+
) -> list[OsidbApiV2AffectsListFlawWorkflowStateItem] | None:
|
|
388
|
+
if not values:
|
|
389
|
+
return None
|
|
390
|
+
return [OsidbApiV2AffectsListFlawWorkflowStateItem(v) for v in values]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def affects_list(
|
|
394
|
+
*,
|
|
395
|
+
ps_module: str | None = None,
|
|
396
|
+
ps_module_in: list[str] | None = None,
|
|
397
|
+
ps_component: str | None = None,
|
|
398
|
+
ps_component_in: list[str] | None = None,
|
|
399
|
+
ps_update_stream: str | None = None,
|
|
400
|
+
ps_update_stream_in: list[str] | None = None,
|
|
401
|
+
flaw_cve_id: str | None = None,
|
|
402
|
+
flaw_cve_id_in: list[str] | None = None,
|
|
403
|
+
flaw_workflow_state: list[str] | None = None,
|
|
404
|
+
flaw_workflow_state_in: list[str] | None = None,
|
|
405
|
+
flaw_impact: str | None = None,
|
|
406
|
+
flaw_impact_in: list[str] | None = None,
|
|
407
|
+
flaw_components: list[str] | None = None,
|
|
408
|
+
flaw_components_in: list[str] | None = None,
|
|
409
|
+
embargoed: bool | None = None,
|
|
410
|
+
include_fields: list[str] | None = None,
|
|
411
|
+
exclude_fields: list[str] | None = None,
|
|
412
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
413
|
+
offset: int = 0,
|
|
414
|
+
api_version: str | None = None,
|
|
415
|
+
extra_query: dict[str, Any] | None = None,
|
|
416
|
+
) -> dict[str, Any]:
|
|
417
|
+
lim = clamp_limit(limit)
|
|
418
|
+
off = clamp_offset(offset)
|
|
419
|
+
try:
|
|
420
|
+
kw: dict[str, Any] = {"limit": lim, "offset": off}
|
|
421
|
+
if ps_module:
|
|
422
|
+
kw["ps_module"] = ps_module
|
|
423
|
+
if ps_module_in:
|
|
424
|
+
kw["ps_module__in"] = ps_module_in
|
|
425
|
+
if ps_component:
|
|
426
|
+
kw["ps_component"] = ps_component
|
|
427
|
+
if ps_component_in:
|
|
428
|
+
kw["ps_component__in"] = ps_component_in
|
|
429
|
+
if ps_update_stream:
|
|
430
|
+
kw["ps_update_stream"] = ps_update_stream
|
|
431
|
+
if ps_update_stream_in:
|
|
432
|
+
kw["ps_update_stream__in"] = ps_update_stream_in
|
|
433
|
+
if flaw_cve_id:
|
|
434
|
+
kw["flaw__cve_id"] = flaw_cve_id
|
|
435
|
+
if flaw_cve_id_in:
|
|
436
|
+
kw["flaw__cve_id__in"] = flaw_cve_id_in
|
|
437
|
+
if flaw_workflow_state:
|
|
438
|
+
kw["flaw__workflow_state"] = _affects_workflow_in(flaw_workflow_state)
|
|
439
|
+
if flaw_workflow_state_in:
|
|
440
|
+
kw["flaw__workflow_state__in"] = _affects_workflow_in(
|
|
441
|
+
flaw_workflow_state_in
|
|
442
|
+
)
|
|
443
|
+
if flaw_impact:
|
|
444
|
+
kw["flaw__impact"] = OsidbApiV2AffectsListFlawImpact(flaw_impact)
|
|
445
|
+
if flaw_impact_in:
|
|
446
|
+
kw["flaw__impact__in"] = _affects_impact_in(flaw_impact_in)
|
|
447
|
+
if flaw_components:
|
|
448
|
+
kw["flaw__components"] = flaw_components
|
|
449
|
+
if flaw_components_in:
|
|
450
|
+
kw["flaw__components__in"] = flaw_components_in
|
|
451
|
+
if embargoed is not None:
|
|
452
|
+
kw["embargoed"] = embargoed
|
|
453
|
+
if include_fields:
|
|
454
|
+
kw["include_fields"] = include_fields
|
|
455
|
+
if exclude_fields:
|
|
456
|
+
kw["exclude_fields"] = exclude_fields
|
|
457
|
+
merged = merge_extra_query(kw, extra_query, allowlist=AFFECTS_EXTRA_KEYS)
|
|
458
|
+
resp = get_session().affects.retrieve_list(
|
|
459
|
+
api_version=api_version,
|
|
460
|
+
**merged,
|
|
461
|
+
)
|
|
462
|
+
return {
|
|
463
|
+
"ok": True,
|
|
464
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
465
|
+
}
|
|
466
|
+
except (requests.RequestException, ValueError) as e:
|
|
467
|
+
if isinstance(e, ValueError):
|
|
468
|
+
return {"ok": False, "error": "bad_request", "detail": str(e)}
|
|
469
|
+
return {"ok": False, **http_error_payload(e)}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _finalize_trackers_kwargs(merged: dict[str, Any]) -> dict[str, Any]:
|
|
473
|
+
"""Map OpenAPI ``type`` query keys to the generated client ``type_`` kwargs."""
|
|
474
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_trackers_list_type import (
|
|
475
|
+
OsidbApiV2TrackersListType,
|
|
476
|
+
)
|
|
477
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_trackers_list_type_in_item import (
|
|
478
|
+
OsidbApiV2TrackersListTypeInItem,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
out = dict(merged)
|
|
482
|
+
if "type" in out:
|
|
483
|
+
v = out.pop("type")
|
|
484
|
+
out["type_"] = (
|
|
485
|
+
OsidbApiV2TrackersListType(v) if isinstance(v, str) else v
|
|
486
|
+
)
|
|
487
|
+
if "type__in" in out:
|
|
488
|
+
v = out.pop("type__in")
|
|
489
|
+
if isinstance(v, list):
|
|
490
|
+
out["type_in"] = [
|
|
491
|
+
OsidbApiV2TrackersListTypeInItem(x) if isinstance(x, str) else x
|
|
492
|
+
for x in v
|
|
493
|
+
]
|
|
494
|
+
else:
|
|
495
|
+
out["type_in"] = v
|
|
496
|
+
return out
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def trackers_list(
|
|
500
|
+
*,
|
|
501
|
+
affects_flaw_cve_id: str | None = None,
|
|
502
|
+
affects_flaw_cve_id_in: list[str] | None = None,
|
|
503
|
+
affects_ps_module_in: list[str] | None = None,
|
|
504
|
+
affects_ps_component_in: list[str] | None = None,
|
|
505
|
+
tracker_type: str | None = None,
|
|
506
|
+
include_fields: list[str] | None = None,
|
|
507
|
+
exclude_fields: list[str] | None = None,
|
|
508
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
509
|
+
offset: int = 0,
|
|
510
|
+
api_version: str | None = None,
|
|
511
|
+
extra_query: dict[str, Any] | None = None,
|
|
512
|
+
) -> dict[str, Any]:
|
|
513
|
+
lim = clamp_limit(limit)
|
|
514
|
+
off = clamp_offset(offset)
|
|
515
|
+
try:
|
|
516
|
+
from osidb_bindings.bindings.python_client.models.osidb_api_v2_trackers_list_type import (
|
|
517
|
+
OsidbApiV2TrackersListType,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
kw: dict[str, Any] = {"limit": lim, "offset": off}
|
|
521
|
+
if affects_flaw_cve_id:
|
|
522
|
+
kw["affects__flaw__cve_id"] = affects_flaw_cve_id
|
|
523
|
+
if affects_flaw_cve_id_in:
|
|
524
|
+
kw["affects__flaw__cve_id__in"] = affects_flaw_cve_id_in
|
|
525
|
+
if affects_ps_module_in:
|
|
526
|
+
kw["affects__ps_module__in"] = affects_ps_module_in
|
|
527
|
+
if affects_ps_component_in:
|
|
528
|
+
kw["affects__ps_component__in"] = affects_ps_component_in
|
|
529
|
+
if tracker_type:
|
|
530
|
+
kw["type_"] = OsidbApiV2TrackersListType(tracker_type)
|
|
531
|
+
if include_fields:
|
|
532
|
+
kw["include_fields"] = include_fields
|
|
533
|
+
if exclude_fields:
|
|
534
|
+
kw["exclude_fields"] = exclude_fields
|
|
535
|
+
merged = merge_extra_query(
|
|
536
|
+
kw, extra_query, allowlist=TRACKERS_EXTRA_KEYS
|
|
537
|
+
)
|
|
538
|
+
merged = _finalize_trackers_kwargs(merged)
|
|
539
|
+
resp = get_session().trackers.retrieve_list(
|
|
540
|
+
api_version=api_version,
|
|
541
|
+
**merged,
|
|
542
|
+
)
|
|
543
|
+
return {
|
|
544
|
+
"ok": True,
|
|
545
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
546
|
+
}
|
|
547
|
+
except (requests.RequestException, ValueError) as e:
|
|
548
|
+
if isinstance(e, ValueError):
|
|
549
|
+
return {"ok": False, "error": "bad_request", "detail": str(e)}
|
|
550
|
+
return {"ok": False, **http_error_payload(e)}
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def flaw_comments_list(
|
|
554
|
+
flaw_id: str,
|
|
555
|
+
*,
|
|
556
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
557
|
+
offset: int = 0,
|
|
558
|
+
api_version: str | None = None,
|
|
559
|
+
) -> dict[str, Any]:
|
|
560
|
+
lim = clamp_limit(limit)
|
|
561
|
+
off = clamp_offset(offset)
|
|
562
|
+
try:
|
|
563
|
+
resp = get_session().flaws.comments.retrieve_list(
|
|
564
|
+
flaw_id,
|
|
565
|
+
api_version=api_version,
|
|
566
|
+
limit=lim,
|
|
567
|
+
offset=off,
|
|
568
|
+
)
|
|
569
|
+
return {
|
|
570
|
+
"ok": True,
|
|
571
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
572
|
+
}
|
|
573
|
+
except requests.RequestException as e:
|
|
574
|
+
return {"ok": False, **http_error_payload(e)}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def flaw_references_list(
|
|
578
|
+
flaw_id: str,
|
|
579
|
+
*,
|
|
580
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
581
|
+
offset: int = 0,
|
|
582
|
+
api_version: str | None = None,
|
|
583
|
+
) -> dict[str, Any]:
|
|
584
|
+
lim = clamp_limit(limit)
|
|
585
|
+
off = clamp_offset(offset)
|
|
586
|
+
try:
|
|
587
|
+
resp = get_session().flaws.references.retrieve_list(
|
|
588
|
+
flaw_id,
|
|
589
|
+
api_version=api_version,
|
|
590
|
+
limit=lim,
|
|
591
|
+
offset=off,
|
|
592
|
+
)
|
|
593
|
+
return {
|
|
594
|
+
"ok": True,
|
|
595
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
596
|
+
}
|
|
597
|
+
except requests.RequestException as e:
|
|
598
|
+
return {"ok": False, **http_error_payload(e)}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def flaw_cvss_scores_list(
|
|
602
|
+
flaw_id: str,
|
|
603
|
+
*,
|
|
604
|
+
limit: int = DEFAULT_LIST_LIMIT,
|
|
605
|
+
offset: int = 0,
|
|
606
|
+
api_version: str | None = None,
|
|
607
|
+
) -> dict[str, Any]:
|
|
608
|
+
lim = clamp_limit(limit)
|
|
609
|
+
off = clamp_offset(offset)
|
|
610
|
+
try:
|
|
611
|
+
resp = get_session().flaws.cvss_scores.retrieve_list(
|
|
612
|
+
flaw_id,
|
|
613
|
+
api_version=api_version,
|
|
614
|
+
limit=lim,
|
|
615
|
+
offset=off,
|
|
616
|
+
)
|
|
617
|
+
return {
|
|
618
|
+
"ok": True,
|
|
619
|
+
**paginated_summary(resp, limit=lim, offset=off),
|
|
620
|
+
}
|
|
621
|
+
except requests.RequestException as e:
|
|
622
|
+
return {"ok": False, **http_error_payload(e)}
|