vulnotes 0.1.0__py3-none-any.whl
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.
- vulnotes/__init__.py +42 -0
- vulnotes/_utils.py +65 -0
- vulnotes/_version.py +1 -0
- vulnotes/client.py +228 -0
- vulnotes/exceptions.py +122 -0
- vulnotes/resources/__init__.py +31 -0
- vulnotes/resources/_base.py +67 -0
- vulnotes/resources/ai.py +110 -0
- vulnotes/resources/api_keys.py +17 -0
- vulnotes/resources/attachments.py +75 -0
- vulnotes/resources/comments.py +98 -0
- vulnotes/resources/companies.py +77 -0
- vulnotes/resources/findings.py +62 -0
- vulnotes/resources/images.py +85 -0
- vulnotes/resources/notes.py +71 -0
- vulnotes/resources/planning.py +210 -0
- vulnotes/resources/report_templates.py +52 -0
- vulnotes/resources/reports.py +293 -0
- vulnotes/resources/snapshots.py +35 -0
- vulnotes/resources/vulnerabilities.py +82 -0
- vulnotes/resources/vulnerability_templates.py +132 -0
- vulnotes-0.1.0.dist-info/METADATA +166 -0
- vulnotes-0.1.0.dist-info/RECORD +25 -0
- vulnotes-0.1.0.dist-info/WHEEL +4 -0
- vulnotes-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Report templates (read-only surface). Permission: ``ro:templates``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .._utils import omit_none
|
|
10
|
+
from ._base import JSON, Resource, page_params, paginate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReportTemplates(Resource):
|
|
14
|
+
def list(self, *, page: int | None = None, limit: int | None = None) -> JSON:
|
|
15
|
+
"""List report templates (plain list, or paginated envelope with ``page``/``limit``)."""
|
|
16
|
+
return self._client.get("/templates", params=page_params(page, limit))
|
|
17
|
+
|
|
18
|
+
def iter(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
|
|
19
|
+
"""Iterate over every report template, fetching pages lazily."""
|
|
20
|
+
return paginate(lambda page, limit: self.list(page=page, limit=limit), limit=limit)
|
|
21
|
+
|
|
22
|
+
def search(self, query: str) -> builtins.list[dict[str, Any]]:
|
|
23
|
+
return self._client.get("/templates/search", params={"query": query})
|
|
24
|
+
|
|
25
|
+
def get(self, template_id: str) -> dict[str, Any]:
|
|
26
|
+
return self._client.get(f"/templates/{template_id}")
|
|
27
|
+
|
|
28
|
+
def content(self, template_id: str) -> dict[str, Any]:
|
|
29
|
+
"""Get the template's HTML content."""
|
|
30
|
+
return self._client.get(f"/templates/{template_id}/content")
|
|
31
|
+
|
|
32
|
+
def revisions(self, template_id: str) -> builtins.list[dict[str, Any]]:
|
|
33
|
+
"""List the template's content revisions."""
|
|
34
|
+
resp = self._client.get(f"/templates/{template_id}/revisions")
|
|
35
|
+
if isinstance(resp, dict):
|
|
36
|
+
return resp.get("revisions", [])
|
|
37
|
+
return resp
|
|
38
|
+
|
|
39
|
+
def revision(self, template_id: str, revision_id: str) -> dict[str, Any]:
|
|
40
|
+
"""Get a single content revision."""
|
|
41
|
+
return self._client.get(f"/templates/{template_id}/revisions/{revision_id}")
|
|
42
|
+
|
|
43
|
+
def preview(
|
|
44
|
+
self,
|
|
45
|
+
template_id: str,
|
|
46
|
+
*,
|
|
47
|
+
report_id: str | None = None,
|
|
48
|
+
snapshot_id: str | None = None,
|
|
49
|
+
) -> dict[str, Any]:
|
|
50
|
+
"""Preview the template rendered with a report's (or snapshot's) data."""
|
|
51
|
+
params = omit_none({"reportId": report_id, "snapshotId": snapshot_id})
|
|
52
|
+
return self._client.get(f"/templates/{template_id}/preview", params=params)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Reports: lifecycle, search, import/export. Permissions: ``ro:reports`` / ``rw:reports``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
import datetime as _dt
|
|
7
|
+
import os
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from typing import Any, Union
|
|
10
|
+
|
|
11
|
+
from .._utils import iso, omit_none, write_bytes
|
|
12
|
+
from ._base import JSON, Resource, page_params, paginate
|
|
13
|
+
|
|
14
|
+
DateLike = Union[str, _dt.date, _dt.datetime]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Reports(Resource):
|
|
18
|
+
def list(self, *, page: int | None = None, limit: int | None = None) -> JSON:
|
|
19
|
+
"""List reports (plain list, or paginated envelope with ``page``/``limit``).
|
|
20
|
+
|
|
21
|
+
Visibility: if the key owner's team uses contributors-only report
|
|
22
|
+
visibility, only reports they created, contribute to, or review are
|
|
23
|
+
returned.
|
|
24
|
+
"""
|
|
25
|
+
return self._client.get("/reports", params=page_params(page, limit))
|
|
26
|
+
|
|
27
|
+
def iter(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
|
|
28
|
+
"""Iterate over every visible report, fetching pages lazily."""
|
|
29
|
+
return paginate(lambda page, limit: self.list(page=page, limit=limit), limit=limit)
|
|
30
|
+
|
|
31
|
+
def search(self, query: str) -> builtins.list[dict[str, Any]]:
|
|
32
|
+
return self._client.get("/reports/search", params={"query": query})
|
|
33
|
+
|
|
34
|
+
def get(self, report_id: str) -> dict[str, Any]:
|
|
35
|
+
return self._client.get(f"/reports/{report_id}")
|
|
36
|
+
|
|
37
|
+
def create(
|
|
38
|
+
self,
|
|
39
|
+
title: str,
|
|
40
|
+
*,
|
|
41
|
+
company: str | None = None,
|
|
42
|
+
template: str | None = None,
|
|
43
|
+
vuln_template: str | None = None,
|
|
44
|
+
language: str | None = None,
|
|
45
|
+
start_date: DateLike | None = None,
|
|
46
|
+
end_date: DateLike | None = None,
|
|
47
|
+
scope: Any | None = None,
|
|
48
|
+
contributors: builtins.list[str] | None = None,
|
|
49
|
+
contacts: builtins.list[dict[str, Any]] | None = None,
|
|
50
|
+
planning_event_id: str | None = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""Create a report. Requires ``rw:reports``.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
title: Report title (stored verbatim).
|
|
56
|
+
company: Company ObjectId.
|
|
57
|
+
template: Report template ObjectId.
|
|
58
|
+
vuln_template: Vulnerability template ObjectId.
|
|
59
|
+
language: Report language code, e.g. ``"EN"``.
|
|
60
|
+
start_date / end_date: Engagement dates (str, date or datetime).
|
|
61
|
+
scope: Engagement scope, shaped as ``{"description": str,
|
|
62
|
+
"entries": [{"type": "ip"|"url"|"other", "name": str,
|
|
63
|
+
"value": str}]}``.
|
|
64
|
+
contributors: User ObjectIds.
|
|
65
|
+
contacts: Contact objects.
|
|
66
|
+
planning_event_id: Planning event to link the report to.
|
|
67
|
+
"""
|
|
68
|
+
body = omit_none(
|
|
69
|
+
{
|
|
70
|
+
"title": title,
|
|
71
|
+
"company": company,
|
|
72
|
+
"template": template,
|
|
73
|
+
"vulnTemplate": vuln_template,
|
|
74
|
+
"language": language,
|
|
75
|
+
"startDate": iso(start_date),
|
|
76
|
+
"endDate": iso(end_date),
|
|
77
|
+
"scope": scope,
|
|
78
|
+
"contributors": contributors,
|
|
79
|
+
"contacts": contacts,
|
|
80
|
+
"planningEventId": planning_event_id,
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return self._client.post("/reports", json=body)
|
|
84
|
+
|
|
85
|
+
def update(
|
|
86
|
+
self,
|
|
87
|
+
report_id: str,
|
|
88
|
+
*,
|
|
89
|
+
title: str | None = None,
|
|
90
|
+
status: str | None = None,
|
|
91
|
+
company: str | None = None,
|
|
92
|
+
contacts: builtins.list[dict[str, Any]] | None = None,
|
|
93
|
+
template: str | None = None,
|
|
94
|
+
remove_template: bool | None = None,
|
|
95
|
+
vulnerability_template: str | None = None,
|
|
96
|
+
remove_vulnerability_template: bool | None = None,
|
|
97
|
+
language: str | None = None,
|
|
98
|
+
start_date: DateLike | None = None,
|
|
99
|
+
end_date: DateLike | None = None,
|
|
100
|
+
scope: Any | None = None,
|
|
101
|
+
content: Any | None = None,
|
|
102
|
+
contributors: builtins.list[str] | None = None,
|
|
103
|
+
findings: builtins.list[dict[str, Any]] | None = None,
|
|
104
|
+
archive_html: str | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Update a report. Only the supplied fields are changed.
|
|
107
|
+
|
|
108
|
+
Setting ``status="completed"`` archives a PDF of the report; pass
|
|
109
|
+
``archive_html`` to control the archived rendering.
|
|
110
|
+
"""
|
|
111
|
+
body = omit_none(
|
|
112
|
+
{
|
|
113
|
+
"title": title,
|
|
114
|
+
"status": status,
|
|
115
|
+
"company": company,
|
|
116
|
+
"contacts": contacts,
|
|
117
|
+
"template": template,
|
|
118
|
+
"removeTemplate": remove_template,
|
|
119
|
+
"vulnerabilityTemplate": vulnerability_template,
|
|
120
|
+
"removeVulnerabilityTemplate": remove_vulnerability_template,
|
|
121
|
+
"language": language,
|
|
122
|
+
"startDate": iso(start_date),
|
|
123
|
+
"endDate": iso(end_date),
|
|
124
|
+
"scope": scope,
|
|
125
|
+
"content": content,
|
|
126
|
+
"contributors": contributors,
|
|
127
|
+
"findings": findings,
|
|
128
|
+
"archiveHtml": archive_html,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return self._client.put(f"/reports/{report_id}", json=body)
|
|
132
|
+
|
|
133
|
+
def delete(self, report_id: str) -> JSON:
|
|
134
|
+
return self._client.delete(f"/reports/{report_id}")
|
|
135
|
+
|
|
136
|
+
def sync_from_template(self, template_id: str) -> JSON:
|
|
137
|
+
"""Re-sync every report built from the given template with its latest content."""
|
|
138
|
+
return self._client.post(f"/reports/sync-from-template/{template_id}")
|
|
139
|
+
|
|
140
|
+
# ── import / export ──────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def import_json(
|
|
143
|
+
self,
|
|
144
|
+
export: dict[str, Any],
|
|
145
|
+
*,
|
|
146
|
+
import_options: dict[str, Any] | None = None,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Import a report from a Vulnotes JSON export.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
export: A dict as produced by :meth:`export_json`
|
|
152
|
+
(``exportType`` and ``report`` are required).
|
|
153
|
+
import_options: Optional import behaviour overrides; merged into
|
|
154
|
+
the payload as ``importOptions``.
|
|
155
|
+
"""
|
|
156
|
+
body = dict(export)
|
|
157
|
+
if import_options is not None:
|
|
158
|
+
body["importOptions"] = import_options
|
|
159
|
+
return self._client.post("/reports/import", json=body)
|
|
160
|
+
|
|
161
|
+
def export_json(self, report_id: str) -> dict[str, Any]:
|
|
162
|
+
"""Export the full report with all related data as a self-contained dict."""
|
|
163
|
+
return self._client.get(f"/reports/{report_id}/export/json")
|
|
164
|
+
|
|
165
|
+
def export_pdf(
|
|
166
|
+
self,
|
|
167
|
+
report_id: str,
|
|
168
|
+
*,
|
|
169
|
+
html: str | None = None,
|
|
170
|
+
file_name: str | None = None,
|
|
171
|
+
path: str | os.PathLike[str] | None = None,
|
|
172
|
+
timeout: float | None = 300.0,
|
|
173
|
+
) -> bytes:
|
|
174
|
+
"""Export the report as a PDF and return the raw bytes.
|
|
175
|
+
|
|
176
|
+
Without ``html`` this uses the legacy server-side rendering endpoint.
|
|
177
|
+
Note that server-side rendering does not support every layout feature;
|
|
178
|
+
for a pixel-perfect export pass pre-rendered ``html`` from the app's
|
|
179
|
+
preview. Requires a valid, non-expired license.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
html: Pre-rendered HTML to generate the PDF from.
|
|
183
|
+
file_name: Download filename hint.
|
|
184
|
+
path: If given, also write the PDF to this file.
|
|
185
|
+
timeout: PDF generation can be slow; defaults to 300 seconds.
|
|
186
|
+
"""
|
|
187
|
+
if html is not None:
|
|
188
|
+
content = self._client.post(
|
|
189
|
+
f"/reports/{report_id}/export/pdf",
|
|
190
|
+
json=omit_none({"html": html, "fileName": file_name}),
|
|
191
|
+
raw=True,
|
|
192
|
+
timeout=timeout,
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
content = self._client.get(
|
|
196
|
+
f"/reports/{report_id}/export/pdf", raw=True, timeout=timeout
|
|
197
|
+
)
|
|
198
|
+
if path is not None:
|
|
199
|
+
write_bytes(content, path)
|
|
200
|
+
return content
|
|
201
|
+
|
|
202
|
+
def export_docx(
|
|
203
|
+
self,
|
|
204
|
+
report_id: str,
|
|
205
|
+
html: str,
|
|
206
|
+
*,
|
|
207
|
+
header_html: str | None = None,
|
|
208
|
+
footer_html: str | None = None,
|
|
209
|
+
landscape_header_html: str | None = None,
|
|
210
|
+
landscape_footer_html: str | None = None,
|
|
211
|
+
first_header_html: str | None = None,
|
|
212
|
+
file_name: str | None = None,
|
|
213
|
+
options: dict[str, Any] | None = None,
|
|
214
|
+
path: str | os.PathLike[str] | None = None,
|
|
215
|
+
timeout: float | None = 300.0,
|
|
216
|
+
) -> bytes:
|
|
217
|
+
"""Export the report as DOCX from pre-rendered HTML; returns raw bytes."""
|
|
218
|
+
body = omit_none(
|
|
219
|
+
{
|
|
220
|
+
"html": html,
|
|
221
|
+
"headerHtml": header_html,
|
|
222
|
+
"footerHtml": footer_html,
|
|
223
|
+
"landscapeHeaderHtml": landscape_header_html,
|
|
224
|
+
"landscapeFooterHtml": landscape_footer_html,
|
|
225
|
+
"firstHeaderHtml": first_header_html,
|
|
226
|
+
"fileName": file_name,
|
|
227
|
+
"options": options,
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
content = self._client.post(
|
|
231
|
+
f"/reports/{report_id}/export/docx", json=body, raw=True, timeout=timeout
|
|
232
|
+
)
|
|
233
|
+
if path is not None:
|
|
234
|
+
write_bytes(content, path)
|
|
235
|
+
return content
|
|
236
|
+
|
|
237
|
+
def export_xlsx(
|
|
238
|
+
self,
|
|
239
|
+
report_id: str,
|
|
240
|
+
*,
|
|
241
|
+
finding_fields: builtins.list[str] | None = None,
|
|
242
|
+
content_sections: builtins.list[str] | None = None,
|
|
243
|
+
path: str | os.PathLike[str] | None = None,
|
|
244
|
+
timeout: float | None = 120.0,
|
|
245
|
+
) -> bytes:
|
|
246
|
+
"""Export the report's findings as an XLSX spreadsheet; returns raw bytes.
|
|
247
|
+
|
|
248
|
+
The API requires at least one finding field or content section to be
|
|
249
|
+
selected, e.g. ``finding_fields=["title", "severity"]``.
|
|
250
|
+
"""
|
|
251
|
+
body = omit_none(
|
|
252
|
+
{"findingFields": finding_fields, "contentSections": content_sections}
|
|
253
|
+
)
|
|
254
|
+
content = self._client.post(
|
|
255
|
+
f"/reports/{report_id}/export/xlsx", json=body, raw=True, timeout=timeout
|
|
256
|
+
)
|
|
257
|
+
if path is not None:
|
|
258
|
+
write_bytes(content, path)
|
|
259
|
+
return content
|
|
260
|
+
|
|
261
|
+
def export_zip(
|
|
262
|
+
self,
|
|
263
|
+
report_id: str,
|
|
264
|
+
html: str,
|
|
265
|
+
*,
|
|
266
|
+
file_name: str | None = None,
|
|
267
|
+
password: str | None = None,
|
|
268
|
+
path: str | os.PathLike[str] | None = None,
|
|
269
|
+
timeout: float | None = 300.0,
|
|
270
|
+
) -> bytes:
|
|
271
|
+
"""Export the report as a (optionally password-protected) ZIP archive."""
|
|
272
|
+
body = omit_none({"html": html, "fileName": file_name, "password": password})
|
|
273
|
+
content = self._client.post(
|
|
274
|
+
f"/reports/{report_id}/export/zip", json=body, raw=True, timeout=timeout
|
|
275
|
+
)
|
|
276
|
+
if path is not None:
|
|
277
|
+
write_bytes(content, path)
|
|
278
|
+
return content
|
|
279
|
+
|
|
280
|
+
def archived_pdf(
|
|
281
|
+
self,
|
|
282
|
+
report_id: str,
|
|
283
|
+
*,
|
|
284
|
+
path: str | os.PathLike[str] | None = None,
|
|
285
|
+
) -> bytes:
|
|
286
|
+
"""Download the PDF archived when the report was marked completed.
|
|
287
|
+
|
|
288
|
+
Raises :class:`~vulnotes.exceptions.NotFoundError` if no archived PDF exists.
|
|
289
|
+
"""
|
|
290
|
+
content = self._client.get(f"/reports/{report_id}/archive", raw=True)
|
|
291
|
+
if path is not None:
|
|
292
|
+
write_bytes(content, path)
|
|
293
|
+
return content
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Report snapshots (review states). Permissions: ``ro:reports`` / ``rw:reports``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import builtins
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ._base import Resource
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Snapshots(Resource):
|
|
12
|
+
def list(self, report_id: str) -> builtins.list[dict[str, Any]]:
|
|
13
|
+
"""Get the snapshot history for a report."""
|
|
14
|
+
return self._client.get(f"/reports/{report_id}/snapshots")
|
|
15
|
+
|
|
16
|
+
def create(self, report_id: str) -> dict[str, Any]:
|
|
17
|
+
"""Create a manual snapshot of the report's current state."""
|
|
18
|
+
return self._client.post(f"/reports/{report_id}/snapshots")
|
|
19
|
+
|
|
20
|
+
def active(self, report_id: str) -> dict[str, Any]:
|
|
21
|
+
"""Get the report's active snapshot."""
|
|
22
|
+
return self._client.get(f"/reports/{report_id}/snapshots/active")
|
|
23
|
+
|
|
24
|
+
def get(self, report_id: str, snapshot_id: str) -> dict[str, Any]:
|
|
25
|
+
return self._client.get(f"/reports/{report_id}/snapshots/{snapshot_id}")
|
|
26
|
+
|
|
27
|
+
def diff(self, report_id: str, snapshot_id: str) -> dict[str, Any]:
|
|
28
|
+
"""Diff a snapshot against the current report state."""
|
|
29
|
+
return self._client.get(f"/reports/{report_id}/snapshots/{snapshot_id}/diff")
|
|
30
|
+
|
|
31
|
+
def preview_data(self, report_id: str, snapshot_id: str) -> dict[str, Any]:
|
|
32
|
+
"""Get the data needed to render a preview of the snapshot."""
|
|
33
|
+
return self._client.get(
|
|
34
|
+
f"/reports/{report_id}/snapshots/{snapshot_id}/preview-data"
|
|
35
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The reusable vulnerability library.
|
|
2
|
+
|
|
3
|
+
Permissions: ``ro:vulnerabilities`` / ``rw:vulnerabilities``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .._utils import omit_none
|
|
13
|
+
from ._base import JSON, Resource, page_params, paginate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Vulnerabilities(Resource):
|
|
17
|
+
def list(self, *, page: int | None = None, limit: int | None = None) -> JSON:
|
|
18
|
+
"""List vulnerabilities (plain list, or paginated envelope with ``page``/``limit``)."""
|
|
19
|
+
return self._client.get("/vulnerabilities", params=page_params(page, limit))
|
|
20
|
+
|
|
21
|
+
def iter(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
|
|
22
|
+
"""Iterate over every vulnerability in the library, fetching pages lazily."""
|
|
23
|
+
return paginate(lambda page, limit: self.list(page=page, limit=limit), limit=limit)
|
|
24
|
+
|
|
25
|
+
def search(self, query: str) -> builtins.list[dict[str, Any]]:
|
|
26
|
+
"""Search the vulnerability library (accent-insensitive)."""
|
|
27
|
+
return self._client.get("/vulnerabilities/search", params={"query": query})
|
|
28
|
+
|
|
29
|
+
def get(self, vulnerability_id: str) -> dict[str, Any]:
|
|
30
|
+
return self._client.get(f"/vulnerabilities/{vulnerability_id}")
|
|
31
|
+
|
|
32
|
+
def create(
|
|
33
|
+
self,
|
|
34
|
+
title: str,
|
|
35
|
+
*,
|
|
36
|
+
category: str | None = None,
|
|
37
|
+
template_id: str | None = None,
|
|
38
|
+
languages: builtins.list[str] | None = None,
|
|
39
|
+
data: dict[str, Any] | None = None,
|
|
40
|
+
) -> dict[str, Any]:
|
|
41
|
+
"""Create a library vulnerability. Requires ``rw:vulnerabilities``.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
title: Vulnerability title.
|
|
45
|
+
category: Free-form category.
|
|
46
|
+
template_id: Vulnerability template this entry conforms to.
|
|
47
|
+
languages: Language codes present in ``data``.
|
|
48
|
+
data: Language-keyed field data, e.g. ``{"EN": {"title": ..., "description": ...}}``.
|
|
49
|
+
"""
|
|
50
|
+
body = omit_none(
|
|
51
|
+
{
|
|
52
|
+
"title": title,
|
|
53
|
+
"category": category,
|
|
54
|
+
"templateId": template_id,
|
|
55
|
+
"languages": languages,
|
|
56
|
+
"data": data,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
return self._client.post("/vulnerabilities", json=body)
|
|
60
|
+
|
|
61
|
+
def update(
|
|
62
|
+
self,
|
|
63
|
+
vulnerability_id: str,
|
|
64
|
+
title: str,
|
|
65
|
+
*,
|
|
66
|
+
category: str | None = None,
|
|
67
|
+
template_id: str | None = None,
|
|
68
|
+
data: dict[str, Any] | None = None,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Update a library vulnerability (``title`` is required by the API)."""
|
|
71
|
+
body = omit_none(
|
|
72
|
+
{
|
|
73
|
+
"title": title,
|
|
74
|
+
"category": category,
|
|
75
|
+
"templateId": template_id,
|
|
76
|
+
"data": data,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
return self._client.put(f"/vulnerabilities/{vulnerability_id}", json=body)
|
|
80
|
+
|
|
81
|
+
def delete(self, vulnerability_id: str) -> JSON:
|
|
82
|
+
return self._client.delete(f"/vulnerabilities/{vulnerability_id}")
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Vulnerability templates and their field definitions.
|
|
2
|
+
|
|
3
|
+
Permissions: ``ro:vulnerability_templates`` / ``rw:vulnerability_templates``.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .._utils import omit_none
|
|
13
|
+
from ._base import JSON, Resource, page_params, paginate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VulnerabilityTemplates(Resource):
|
|
17
|
+
def list(self, *, page: int | None = None, limit: int | None = None) -> JSON:
|
|
18
|
+
"""List vulnerability templates (plain list, or paginated envelope
|
|
19
|
+
with ``page``/``limit``)."""
|
|
20
|
+
resp = self._client.get(
|
|
21
|
+
"/vulnerability-templates", params=page_params(page, limit)
|
|
22
|
+
)
|
|
23
|
+
# Unpaginated responses arrive as {success, count, data}; unwrap so
|
|
24
|
+
# this behaves like every other resource's list().
|
|
25
|
+
if isinstance(resp, dict) and "pagination" not in resp:
|
|
26
|
+
return resp.get("data", resp)
|
|
27
|
+
return resp
|
|
28
|
+
|
|
29
|
+
def iter(self, *, limit: int = 100) -> Iterator[dict[str, Any]]:
|
|
30
|
+
"""Iterate over every vulnerability template, fetching pages lazily."""
|
|
31
|
+
return paginate(
|
|
32
|
+
lambda page, limit: self._client.get(
|
|
33
|
+
"/vulnerability-templates", params=page_params(page, limit)
|
|
34
|
+
),
|
|
35
|
+
limit=limit,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def search(self, query: str) -> builtins.list[dict[str, Any]]:
|
|
39
|
+
resp = self._client.get(
|
|
40
|
+
"/vulnerability-templates/search", params={"query": query}
|
|
41
|
+
)
|
|
42
|
+
if isinstance(resp, dict):
|
|
43
|
+
return resp.get("data", [])
|
|
44
|
+
return resp
|
|
45
|
+
|
|
46
|
+
def get(self, template_id: str) -> dict[str, Any]:
|
|
47
|
+
return self._client.get(f"/vulnerability-templates/{template_id}")
|
|
48
|
+
|
|
49
|
+
def create(
|
|
50
|
+
self,
|
|
51
|
+
name: str,
|
|
52
|
+
supported_languages: builtins.list[str],
|
|
53
|
+
fields: builtins.list[dict[str, Any]],
|
|
54
|
+
*,
|
|
55
|
+
description: str | None = None,
|
|
56
|
+
is_multilingual: bool | None = None,
|
|
57
|
+
report_template_ids: builtins.list[str] | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Create a vulnerability template. Requires ``rw:vulnerability_templates``.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
name: Template name.
|
|
63
|
+
supported_languages: Language codes, e.g. ``["EN", "FR"]``.
|
|
64
|
+
fields: Field definitions for findings created from this template.
|
|
65
|
+
Each field requires ``id``, ``name``, ``order`` and ``type``
|
|
66
|
+
(one of ``text``, ``richtext``, ``dropdown``, ``tags``,
|
|
67
|
+
``cvss``, ``number``, ``checkbox``, ``date``,
|
|
68
|
+
``customscore``), e.g. ``{"id": "description",
|
|
69
|
+
"name": "description", "label": "Description",
|
|
70
|
+
"type": "richtext", "order": 1}``.
|
|
71
|
+
description: Optional description.
|
|
72
|
+
is_multilingual: Whether findings hold per-language data.
|
|
73
|
+
report_template_ids: Report templates to associate.
|
|
74
|
+
"""
|
|
75
|
+
body = omit_none(
|
|
76
|
+
{
|
|
77
|
+
"name": name,
|
|
78
|
+
"supportedLanguages": supported_languages,
|
|
79
|
+
"fields": fields,
|
|
80
|
+
"description": description,
|
|
81
|
+
"isMultilingual": is_multilingual,
|
|
82
|
+
"reportTemplateIds": report_template_ids,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
return self._client.post("/vulnerability-templates", json=body)
|
|
86
|
+
|
|
87
|
+
def update(
|
|
88
|
+
self,
|
|
89
|
+
template_id: str,
|
|
90
|
+
name: str,
|
|
91
|
+
supported_languages: builtins.list[str],
|
|
92
|
+
fields: builtins.list[dict[str, Any]],
|
|
93
|
+
*,
|
|
94
|
+
description: str | None = None,
|
|
95
|
+
is_multilingual: bool | None = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Update a vulnerability template.
|
|
98
|
+
|
|
99
|
+
``name``, ``supported_languages`` and ``fields`` are required by the API
|
|
100
|
+
(the update is a full replacement of those fields).
|
|
101
|
+
"""
|
|
102
|
+
body = omit_none(
|
|
103
|
+
{
|
|
104
|
+
"name": name,
|
|
105
|
+
"supportedLanguages": supported_languages,
|
|
106
|
+
"fields": fields,
|
|
107
|
+
"description": description,
|
|
108
|
+
"isMultilingual": is_multilingual,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
return self._client.put(f"/vulnerability-templates/{template_id}", json=body)
|
|
112
|
+
|
|
113
|
+
def delete(self, template_id: str) -> JSON:
|
|
114
|
+
return self._client.delete(f"/vulnerability-templates/{template_id}")
|
|
115
|
+
|
|
116
|
+
def report_templates(self, template_id: str) -> builtins.list[dict[str, Any]]:
|
|
117
|
+
"""List the report templates associated with this vulnerability template."""
|
|
118
|
+
resp = self._client.get(
|
|
119
|
+
f"/vulnerability-templates/{template_id}/report-templates"
|
|
120
|
+
)
|
|
121
|
+
if isinstance(resp, dict):
|
|
122
|
+
return resp.get("data", [])
|
|
123
|
+
return resp
|
|
124
|
+
|
|
125
|
+
def set_report_templates(
|
|
126
|
+
self, template_id: str, report_template_ids: builtins.list[str]
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""Replace the set of report templates linked to this vulnerability template."""
|
|
129
|
+
return self._client.put(
|
|
130
|
+
f"/vulnerability-templates/{template_id}/report-templates",
|
|
131
|
+
json={"reportTemplateIds": report_template_ids},
|
|
132
|
+
)
|