attestationcheck 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,5 @@
1
+ """Entry point for python -m attestationcheck."""
2
+
3
+ from attestationcheck.io.cli import cli, main
4
+
5
+ _ = (cli, main)
@@ -0,0 +1,7 @@
1
+ """Entry point for python -m attestationcheck."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from attestationcheck.io.cli import cli
6
+
7
+ cli()
File without changes
@@ -0,0 +1,175 @@
1
+ """Output the attestations used by dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from dataclasses import fields
7
+ from pathlib import Path
8
+ from sys import exit as sysexit
9
+ from sys import stdin, stdout
10
+
11
+ from configurator import Config
12
+ from configurator.node import ConfigNode
13
+
14
+ from attestationcheck import packageinforesolver # ,checker
15
+ from attestationcheck.io import fmt
16
+ from attestationcheck.models.config import LC_Config
17
+ from attestationcheck.models.packageinfo import PackageInfo
18
+
19
+ stdout.reconfigure(encoding="utf-8") # type: ignore[general-type-issues]
20
+
21
+
22
+ def cli() -> None: # pragma: no cover
23
+ """Cli entry point."""
24
+ parser = argparse.ArgumentParser(description=__doc__, argument_default=argparse.SUPPRESS)
25
+
26
+ parser.add_argument(
27
+ "--format",
28
+ "-f",
29
+ help=f"Output format. one of: {', '.join(set(fmt.formatMap))}. default=simple",
30
+ )
31
+ parser.add_argument(
32
+ "--requirements-paths",
33
+ "-r",
34
+ help="Filenames to read from (omit for stdin if piping, else pyproject.toml)",
35
+ nargs="+",
36
+ )
37
+ parser.add_argument(
38
+ "--groups",
39
+ "-g",
40
+ help="Select groups from supported files",
41
+ nargs="+",
42
+ )
43
+ parser.add_argument(
44
+ "--extras",
45
+ "-e",
46
+ help="Select extras from supported files",
47
+ nargs="+",
48
+ )
49
+ parser.add_argument(
50
+ "--file",
51
+ "-o",
52
+ help="Filename to write output to (omit this for stdout)",
53
+ )
54
+ parser.add_argument(
55
+ "--skip-dependencies",
56
+ help="set of packages/dependencies to skip",
57
+ nargs="+",
58
+ )
59
+ parser.add_argument(
60
+ "--hide-output-parameters",
61
+ help="set of parameters to hide from the produced output",
62
+ nargs="+",
63
+ )
64
+ parser.add_argument(
65
+ "--show-only-failing",
66
+ help="Only output a set of failing packages from this lib",
67
+ action="store_true",
68
+ )
69
+ parser.add_argument(
70
+ "--pypi-api",
71
+ help="Specify a custom pypi api endpoint, for example if using a custom pypi server, "
72
+ "note this must implement the 'pypi' and 'integrity' endpoints",
73
+ )
74
+ parser.add_argument(
75
+ "--zero",
76
+ "-0",
77
+ help="Return non zero exit code if a package with an unverified attestation is found, ideal"
78
+ " for CI/CD",
79
+ action="store_true",
80
+ )
81
+ args = vars(parser.parse_args())
82
+
83
+ stdin_path = Path("__stdin__")
84
+ if not args.get("requirements_paths"):
85
+ if stdin.isatty():
86
+ args["requirements_paths"] = ["pyproject.toml"]
87
+ else:
88
+ stdin_path.write_text("\n".join(stdin.readlines()), encoding="utf-8")
89
+
90
+ config: ConfigNode = Config()
91
+
92
+ # (Parses in the following order: `pyproject.toml`,
93
+ # `setup.cfg`, `attestationcheck.toml`, `attestationcheck.json`,
94
+ # `attestationcheck.ini`, `~/attestationcheck.toml`, `~/attestationcheck.json`, `~/attestationcheck.ini`)
95
+ config_files = [
96
+ "~/attestationcheck.json",
97
+ "~/attestationcheck.toml",
98
+ "attestationcheck.json",
99
+ "attestationcheck.toml",
100
+ "setup.cfg",
101
+ "pyproject.toml",
102
+ ]
103
+
104
+ for file in config_files:
105
+ config += Config.from_path(file, optional=True)
106
+
107
+ scopedData: ConfigNode = config.get("tool", {}).get("attestationcheck", ConfigNode())
108
+ attestationcheckConf: LC_Config = LC_Config.model_validate({**scopedData.data, **args})
109
+
110
+ ec = main(attestationcheckConf)
111
+ stdin_path.unlink(missing_ok=True)
112
+
113
+ sysexit(ec)
114
+
115
+
116
+ def main(attestationcheckConf: LC_Config) -> int:
117
+ """Test entry point."""
118
+ exitCode = 0
119
+
120
+ # File
121
+ requirements_paths = attestationcheckConf.requirements_paths or {"__stdin__"}
122
+ output_file = (
123
+ stdout
124
+ if attestationcheckConf.file in [None, ""]
125
+ else Path(attestationcheckConf.file or "").open("w", encoding="utf-8")
126
+ )
127
+
128
+ package_info_manager = packageinforesolver.PackageInfoManager(
129
+ attestationcheckConf.pypi_api or "https://pypi.org"
130
+ )
131
+
132
+ package_info_manager.resolve_requirements(
133
+ requirements_paths=requirements_paths,
134
+ groups=attestationcheckConf.groups,
135
+ extras=attestationcheckConf.extras,
136
+ skip_dependencies=attestationcheckConf.skip_dependencies,
137
+ )
138
+
139
+ all_packages: set[PackageInfo] = package_info_manager.getPackages()
140
+
141
+ incompatible = any(not x.is_attestation_verified for x in all_packages)
142
+
143
+ # Format the results
144
+ hide_output_parameters = attestationcheckConf.hide_output_parameters
145
+
146
+ available_params = [param.name.upper() for param in fields(PackageInfo)]
147
+ if not all(hop in available_params for hop in hide_output_parameters):
148
+ msg = (
149
+ f"Invalid parameter(s) in `hide_output_parameters`. "
150
+ f"Valid parameters are: {', '.join(available_params)}"
151
+ )
152
+ raise ValueError(msg)
153
+
154
+ format_ = attestationcheckConf.format or "simple"
155
+ if attestationcheckConf.format in fmt.formatMap:
156
+ print(
157
+ fmt.fmt(
158
+ format_,
159
+ sorted(all_packages),
160
+ hide_output_parameters,
161
+ show_only_failing=attestationcheckConf.show_only_failing,
162
+ ),
163
+ file=output_file,
164
+ )
165
+ else:
166
+ exitCode = 2
167
+
168
+ # Exit code of 1 if args.zero
169
+ if attestationcheckConf.zero and incompatible:
170
+ exitCode = 1
171
+
172
+ # Cleanup + exit
173
+ if attestationcheckConf.file not in [None, ""]:
174
+ output_file.close()
175
+ return exitCode
@@ -0,0 +1,272 @@
1
+ """
2
+ The formatter is responsible for outputting the list of PackageInfo[s].
3
+
4
+ The available output formats are defined as follows
5
+
6
+ - ansi: Plain text output with ANSI color codes for terminal display.
7
+ used for simple, color-coded output on the command line.
8
+ - plain: A basic, no-frills plain text format, used when a clean and simple
9
+ textual representation is needed without any additional markup or styling.
10
+ - markdown: A lightweight markup language with plain-text formatting syntax. Ideal
11
+ for creating formatted documents that can be easily converted into HTML for web display.
12
+ - html: A format suitable for rendering in web browsers. (can be styled with CSS
13
+ for more complex presentation.)
14
+ - json: A structured data format. This format representing the list of PackageInfo[s]
15
+ as a JSON object
16
+ - csv: A simple, comma-separated values format. widely used in spreadsheets and
17
+ databases for easy import/export of data.
18
+
19
+ Note that these support the get_filtered_dict method, which allows users
20
+ to hide some of the output via the `--hide-output-parameters` cli flag. In addition
21
+ these support the show_only_failing method, which allows users
22
+ to show only packages that are not Verified with out license via the
23
+ `--show-only-failing` cli flag
24
+
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import csv
30
+ import json
31
+ import re
32
+ from collections import OrderedDict
33
+ from importlib.metadata import PackageNotFoundError, version
34
+ from io import StringIO
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ import markdown as markdownlib
39
+ from rich.console import Console
40
+ from rich.table import Table
41
+
42
+ from attestationcheck.models.packageinfo import AttestationInfo, PackageInfo
43
+
44
+ THISDIR = Path(__file__).resolve().parent
45
+
46
+ try:
47
+ VERSION = version("attestationcheck")
48
+ except PackageNotFoundError:
49
+ VERSION = "dev"
50
+ INFO = {"program": "attestationcheck", "version": VERSION, "license": "MIT LICENSE"}
51
+
52
+
53
+ def stripAnsi(string: str) -> str:
54
+ """
55
+ Strip ansi codes from a given string.
56
+
57
+ Args:
58
+ ----
59
+ string (str): string to strip codes from
60
+
61
+ Returns:
62
+ -------
63
+ str: plaintext, utf-8 string (safe for writing to files)
64
+
65
+ """
66
+ return re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub("", string)
67
+
68
+
69
+ def ansi(
70
+ packages: list[dict[str, Any]],
71
+ ) -> str:
72
+ """
73
+ Format to ansi.
74
+
75
+ :param License myLice: project license
76
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
77
+ :return str: string to send to specified output in ansi format
78
+ """
79
+ string = StringIO()
80
+
81
+ console = Console(file=string, color_system="truecolor", safe_box=False)
82
+
83
+ table = Table(title="\nInfo")
84
+ table.add_column("Item", style="cyan")
85
+ table.add_column("Value", style="magenta")
86
+ _ = [table.add_row(k, v) for k, v in INFO.items()]
87
+
88
+ console.print(table)
89
+
90
+ if len(packages) == 0:
91
+ return f"{string.getvalue()}\nNo packages"
92
+
93
+ errors = [x for x in packages if x.get("httpErrorCode", 0) > 0]
94
+ if len(errors) > 0:
95
+ table = Table(title="\nList Of Errors")
96
+ table.add_column("Package", style="magenta")
97
+ _ = [table.add_row(x.get("name", "?")) for x in errors]
98
+ console.print(table)
99
+
100
+ table = Table(title="\nList Of Packages")
101
+ if name_bool := "name" in packages[0]:
102
+ table.add_column("Package", header_style="magenta")
103
+ if attestation_info := "attestation_info" in packages[0]:
104
+ table.add_column("Attestation Info", header_style="magenta")
105
+
106
+ attestation_info_lookup = {
107
+ AttestationInfo.NONE: "[red]Unsupported[/]",
108
+ AttestationInfo.SUPPORTED: "[yellow]Supported[/]",
109
+ AttestationInfo.PRESENT: "[cyan]Present[/]",
110
+ AttestationInfo.VALID: "[green]Valid[/]",
111
+ AttestationInfo.VERIFIED: "[green]Verified[/]",
112
+ }
113
+ _ = [
114
+ table.add_row(
115
+ *(
116
+ ([x.get("name")] if name_bool else [])
117
+ + (
118
+ [attestation_info_lookup[x.get("attestation_info", AttestationInfo.NONE)]]
119
+ if attestation_info
120
+ else []
121
+ )
122
+ )
123
+ )
124
+ for x in packages
125
+ ]
126
+ console.print(table)
127
+ return string.getvalue()
128
+
129
+
130
+ def plainText(
131
+ packages: list[dict[str, Any]],
132
+ ) -> str:
133
+ """
134
+ Format to plain text.
135
+
136
+ :param License myLice: project license
137
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
138
+ :return str: string to send to specified output in plain text format
139
+
140
+ """
141
+ return stripAnsi(ansi(packages))
142
+
143
+
144
+ def markdown(
145
+ packages: list[dict[str, Any]],
146
+ ) -> str:
147
+ """
148
+ Format to markdown.
149
+
150
+ :param License myLice: project license
151
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
152
+ :return str: string to send to specified output in markdown format
153
+ """
154
+ info = "\n".join(f"- {k}: {v}" for k, v in INFO.items())
155
+ strBuf = [f"## Info\n\n{info}\n\n"]
156
+
157
+ if len(packages) == 0:
158
+ return f"{strBuf[0]}\nNo packages"
159
+
160
+ strBuf.append("## Packages\n\nFind a list of packages below\n")
161
+ packages = sorted(packages, key=lambda i: i.get("name", "?"))
162
+
163
+ # Summary Table
164
+ strBuf.append("|Package|Attestation Info|\n|:--|:--|")
165
+ strBuf.extend([f"|{pkg.get('name')}||{pkg.get('attestation_info')}" for pkg in packages])
166
+
167
+ # Details
168
+ params_use_in_markdown = {
169
+ "author": "Author",
170
+ "is_attestation_verified": "Attestation Verified",
171
+ "is_attestation_valid": "Attestation Valid",
172
+ "is_attestation_present": "Attestation Present",
173
+ "is_supported_publisher": "Attestation Supported",
174
+ "size": "Size",
175
+ }
176
+ for pkg in packages:
177
+ pkg_ordered_dict = OrderedDict(
178
+ (param, pkg[param]) for param in params_use_in_markdown if param in pkg
179
+ )
180
+ strBuf.extend(
181
+ [
182
+ f"\n### {pkg.get('namever')}\n",
183
+ *(f"- {params_use_in_markdown[k]}: {v}" for k, v in pkg_ordered_dict.items()),
184
+ ]
185
+ )
186
+ return "\n".join(strBuf) + "\n"
187
+
188
+
189
+ def html(
190
+ packages: list[dict[str, Any]],
191
+ ) -> str:
192
+ """
193
+ Format to html.
194
+
195
+ :param License myLice: project license
196
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
197
+ :return str: string to send to specified output in html format
198
+ """
199
+ html = markdownlib.markdown(
200
+ markdown(packages=packages),
201
+ extensions=["tables"],
202
+ )
203
+ return (THISDIR / "html.template").read_text("utf-8").replace("{html}", html)
204
+
205
+
206
+ def raw(packages: list[dict[str, Any]]) -> str:
207
+ """
208
+ Format to json.
209
+
210
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
211
+ :return str: string to send to specified output in json format
212
+ """
213
+ return json.dumps(
214
+ {
215
+ "info": INFO,
216
+ "packages": packages,
217
+ },
218
+ indent="\t",
219
+ )
220
+
221
+
222
+ def rawCsv(
223
+ packages: list[dict[str, Any]],
224
+ ) -> str:
225
+ """
226
+ Format to csv.
227
+
228
+ :param list[dict[str, Any]] packages: list of PackageInfo, representes as a dict to format.
229
+ :return str: string to send to specified output in csv format
230
+ """
231
+ if len(packages) == 0:
232
+ return ""
233
+
234
+ string = StringIO()
235
+ writer = csv.DictWriter(string, fieldnames=list(packages[0]), lineterminator="\n")
236
+ writer.writeheader()
237
+ writer.writerows(packages)
238
+ return string.getvalue()
239
+
240
+
241
+ def fmt(
242
+ format_: str,
243
+ packages: list[PackageInfo],
244
+ hide_parameters: set[str] | None = None,
245
+ *,
246
+ show_only_failing: bool = False,
247
+ ) -> str:
248
+ """
249
+ Format to a given format by `format_`.
250
+
251
+ :param list[PackageInfo] packages: list of packages to format.
252
+ :param set[str] hide_parameters: set of parameters to ignore in the output.
253
+ :param bool show_only_failing: output only failing packages, defaults to False.
254
+ :return str: string to send to specified output in ansi format
255
+ """
256
+ hide_parameters = hide_parameters or set()
257
+ if show_only_failing:
258
+ packages = [x for x in packages if not x.is_attestation_verified]
259
+
260
+ pkgs: list[dict[str, Any]] = [x.get_filtered_dict(hide_parameters) for x in packages]
261
+
262
+ return formatMap.get(format_, plainText)(pkgs)
263
+
264
+
265
+ formatMap = {
266
+ "json": raw,
267
+ "markdown": markdown,
268
+ "html": html,
269
+ "csv": rawCsv,
270
+ "ansi": ansi,
271
+ "simple": plainText,
272
+ }
@@ -0,0 +1,69 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <style>
5
+ body {
6
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
7
+ color: #222;
8
+ }
9
+ h2 {
10
+ color: #333;
11
+ margin-bottom: 10px;
12
+ }
13
+ ul {
14
+ list-style-type: none;
15
+ padding: 0;
16
+ }
17
+ li {
18
+ margin-bottom: 5px;
19
+ }
20
+ table {
21
+ border-collapse: collapse;
22
+ width: 100%;
23
+ }
24
+ th,
25
+ td {
26
+ border: 1px solid #ddd;
27
+ padding: 8px;
28
+ text-align: left;
29
+ }
30
+ th {
31
+ background-color: #f2f2f2;
32
+ }
33
+ tr:nth-child(even) {
34
+ background-color: #f2f2f2;
35
+ }
36
+ tr:hover {
37
+ background-color: #ddd;
38
+ }
39
+ h3 {
40
+ color: #444;
41
+ }
42
+ @media (prefers-color-scheme: dark) {
43
+ body {
44
+ background-color: #222;
45
+ color: #eee;
46
+ }
47
+ h2 {
48
+ color: #ccc;
49
+ }
50
+ h3 {
51
+ color: #eee;
52
+ }
53
+ th {
54
+ background-color: #333;
55
+ color: #eee;
56
+ }
57
+ tr:nth-child(even) {
58
+ background-color: #333;
59
+ }
60
+ tr:hover {
61
+ background-color: #444;
62
+ }
63
+ }
64
+ </style>
65
+ </head>
66
+ <body>
67
+ {html}
68
+ </body>
69
+ </html>
File without changes
@@ -0,0 +1,73 @@
1
+ from pydantic import Field
2
+
3
+ from attestationcheck.models.defaultonnone import DefaultOnNoneModel
4
+
5
+
6
+ class Envelope(DefaultOnNoneModel):
7
+ signature: str = ""
8
+ statement: str = ""
9
+
10
+
11
+ class KindVersion(DefaultOnNoneModel):
12
+ kind: str = ""
13
+ version: str = ""
14
+
15
+
16
+ class LogId(DefaultOnNoneModel):
17
+ key_id: str = ""
18
+
19
+
20
+ class InclusionPromise(DefaultOnNoneModel):
21
+ signed_entry_timestamp: str = ""
22
+
23
+
24
+ class Checkpoint(DefaultOnNoneModel):
25
+ envelope: str = ""
26
+
27
+
28
+ class InclusionProof(DefaultOnNoneModel):
29
+ checkpoint: Checkpoint = Checkpoint()
30
+ hashes: list[str] = Field(default_factory=list)
31
+ log_index: str = ""
32
+ root_hash: str = ""
33
+ tree_size: str = ""
34
+
35
+
36
+ class TransparencyEntry(DefaultOnNoneModel):
37
+ canonicalizedBody: str = ""
38
+ inclusionPromise: InclusionPromise = InclusionPromise()
39
+ inclusionProof: InclusionProof = InclusionProof()
40
+
41
+ integratedTime: int = 0
42
+ kindVersion: KindVersion = KindVersion()
43
+ logId: LogId = LogId()
44
+ logIndex: str = ""
45
+
46
+
47
+ class VerificationMaterial(DefaultOnNoneModel):
48
+ certificate: str = ""
49
+ transparency_entries: list[TransparencyEntry] = Field(default_factory=list)
50
+
51
+
52
+ class Attestation(DefaultOnNoneModel):
53
+ envelope: Envelope = Envelope()
54
+ verification_material: VerificationMaterial = VerificationMaterial()
55
+ version: int = 0
56
+
57
+
58
+ class Publisher(DefaultOnNoneModel):
59
+ claims: dict = Field(default_factory=dict)
60
+ environment: str = ""
61
+ kind: str = ""
62
+ repository: str = ""
63
+ workflow: str = ""
64
+
65
+
66
+ class AttestationBundle(DefaultOnNoneModel):
67
+ attestations: list[Attestation] = Field(default_factory=list)
68
+ publisher: Publisher = Publisher()
69
+
70
+
71
+ class Provenance(DefaultOnNoneModel):
72
+ attestation_bundles: list[AttestationBundle]
73
+ version: int = 1
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import field
4
+
5
+ from attestationcheck.models.defaultonnone import DefaultOnNoneModel
6
+
7
+
8
+ class LC_Config(DefaultOnNoneModel):
9
+ """LC_Config type."""
10
+
11
+ file: str | None
12
+ format: str | None
13
+ pypi_api: str | None
14
+ show_only_failing: bool
15
+ zero: bool
16
+
17
+ requirements_paths: set[str] = field(default_factory=set)
18
+ groups: set[str] = field(default_factory=set)
19
+ extras: set[str] = field(default_factory=set)
20
+ skip_dependencies: set[str] = field(default_factory=set)
21
+ hide_output_parameters: set[str] = field(default_factory=set)
@@ -0,0 +1,22 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel, model_validator
4
+
5
+
6
+ class DefaultOnNoneModel(BaseModel):
7
+ @model_validator(mode="before")
8
+ @classmethod
9
+ def default_on_none(cls, values: Any) -> Any | dict[Any, Any]:
10
+ if not isinstance(values, dict):
11
+ return values
12
+
13
+ result = dict(values)
14
+
15
+ for name, field in cls.model_fields.items():
16
+ if name in result and result[name] is None:
17
+ if field.default_factory is not None:
18
+ result[name] = field.default_factory()
19
+ elif field.default is not None:
20
+ result[name] = field.default
21
+
22
+ return result