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.
@@ -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
+ )