Skip to content

Commit 9abcb40

Browse files
authored
_cli: Add command to convert Sigstore bundles to attestations (#105)
Signed-off-by: Facundo Tuesca <[email protected]>
1 parent 6885434 commit 9abcb40

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- The CLI has a new subcommand `convert`, which takes a Sigstore bundle
13+
and converts it to a PEP 740 attestation.
14+
1015
### Changed
1116

1217
- The `Attestation.verify(...)` API has been changed to accept an `offline`

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ positional arguments:
100100
sign Sign one or more inputs
101101
verify Verify one or more inputs
102102
inspect Inspect one or more inputs
103+
convert Convert a Sigstore bundle into a PEP 740 attestation
103104

104105
options:
105106
-h, --help show this help message and exit
@@ -164,6 +165,13 @@ This command downloads the artifact and its provenance from PyPI. The artifact
164165
is then verified against the provenance, while also checking that the provenance's
165166
signing identity matches the repository specified by the user.
166167

168+
### Converting a Sigstore bundle into a PEP 740 Attestation
169+
170+
```bash
171+
pypi-attestations convert --output-file /tmp/rfc8785-0.1.2-py3-none-any.whl.publish.attestation \
172+
test/assets/rfc8785-0.1.2-py3-none-any.whl.sigstore
173+
```
174+
167175

168176
[PEP 740]: https://peps.python.org/pep-0740/
169177

src/pypi_attestations/_cli.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
)
2222
from pydantic import ValidationError
2323
from rfc3986 import exceptions, uri_reference, validators
24+
from sigstore.models import Bundle, InvalidBundle
2425
from sigstore.oidc import IdentityError, IdentityToken, Issuer
2526
from sigstore.sign import SigningContext
2627
from sigstore.verify import policy
2728

2829
from pypi_attestations import Attestation, AttestationError, VerificationError, __version__
2930
from pypi_attestations._impl import (
31+
ConversionError,
3032
Distribution,
3133
GitHubPublisher,
3234
Provenance,
@@ -195,9 +197,28 @@ def _parser() -> argparse.ArgumentParser:
195197
metavar="FILE",
196198
type=Path,
197199
nargs="+",
198-
help="The file to sign",
200+
help="The file to inspect",
201+
)
202+
203+
convert_command = subcommands.add_parser(
204+
name="convert",
205+
help="Convert a Sigstore bundle into a PEP 740 attestation",
206+
parents=[parent_parser],
199207
)
200208

209+
convert_command.add_argument(
210+
"bundle_file",
211+
metavar="BUNDLE_FILE",
212+
type=Path,
213+
help="The Sigstore bundle to convert",
214+
)
215+
216+
convert_command.add_argument(
217+
"--output-file",
218+
required=True,
219+
type=Path,
220+
help="The output file to write the attestation to",
221+
)
201222
return parser
202223

203224

@@ -555,6 +576,26 @@ def _verify_pypi(args: argparse.Namespace) -> None:
555576
_logger.info(f"OK: {dist.name}")
556577

557578

579+
def _convert(args: argparse.Namespace) -> None:
580+
"""Convert a Sigstore bundle into a PEP 740 attestation."""
581+
if not args.bundle_file.exists():
582+
_die(f"Bundle file does not exist: {args.bundle_file}")
583+
584+
if args.output_file.exists():
585+
_die(f"Output file already exists: {args.output_file}")
586+
587+
try:
588+
sigstore_bundle = Bundle.from_json(args.bundle_file.read_bytes())
589+
attestation_object = Attestation.from_bundle(sigstore_bundle)
590+
except (InvalidBundle, json.JSONDecodeError) as e:
591+
_die(f"Invalid Sigstore bundle: {e}")
592+
except ConversionError as e:
593+
_die(f"Failed to convert Sigstore bundle: {e}")
594+
595+
args.output_file.write_text(attestation_object.model_dump_json())
596+
_logger.info(f"Converted Sigstore bundle to attestation: {args.output_file}")
597+
598+
558599
def main() -> None:
559600
"""Dispatch the CLI subcommand."""
560601
parser = _parser()
@@ -578,3 +619,5 @@ def main() -> None:
578619
_verify_pypi(args)
579620
elif args.subcommand == "inspect":
580621
_inspect(args)
622+
elif args.subcommand == "convert":
623+
_convert(args)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":1,"verification_material":{"certificate":"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","transparency_entries":[{"logIndex":"101487427","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1718048845","inclusionPromise":{"signedEntryTimestamp":"MEYCIQDMFnTMqL5k9rhgpIE0VG59hChr3vVjUhcpYwlAb9q0zAIhANP7lHSVABph3Rd4HFVv7aMwRF7zSeMfFMd9yxJt3UDA"},"inclusionProof":{"logIndex":"97323996","rootHash":"La0A7Z2xT6hGMjh2tOlyI64RHw84pUHU3fVHj7TSVy0=","treeSize":"97323998","hashes":["3Rz+4naQLu4GK37I4HSkU2R3JWPm+oZAhzVapTCVt70=","PdxfsYo6NM+vpz093wWsIsusyGJ0DlLkUWuxkYhbnmw=","m+waEsb5wCRc096I+AGAxqWDBZBDJ3duxuWZNbV2ohU=","b9P3OggQy8jNuVpZLhIY2PXTiTc87/hAmDwP4mf07uI=","19K0NhNuHUqJP3I26axUgHyh1gqKNIgYCuXCcw+HYKk=","WhBO//b4F4om9U9MtMQuMJIV8ya5e4lr0UFHcMu/xd0=","s+idSozXH82LBPVH/Z9uhHJpWlieFgssfKTTlih3tE0=","Qu6lPX8kEmsqnAi+VrKGmMuZML4NLLrq8niw0Y3xdxE=","tcJBqniz0pBiR21iqSf205jubz0v9XqBqrVEfocm9NE=","uToMdLmWkBlY0yVYaf9GS/JBKW8dEZ9thyQyI8gHtQ0=","RKCkWYqzT6tUZmc3Jvzbxj9MA/gYAWvM/6Ku2bZRDdM=","rX8ztpnrupitNNHTqrykWKXtm2K1j+1xHpOYrqdXSu0=","t13rsrmj5sbMlY8QMEBToVdUZGeJf7ABzGqDcy0ktwg=","cX3Agx+hP66t1ZLbX/yHbfjU46/3m/VAmWyG/fhxAVc=","sjohk/3DQIfXTgf/5XpwtdF7yNbrf8YykOMHr1CyBYQ=","98enzMaC+x5oCMvIZQA5z8vu2apDMCFvE/935NfuPw8="],"checkpoint":{"envelope":"rekor.sigstore.dev - 2605736670972794746\n97323998\nLa0A7Z2xT6hGMjh2tOlyI64RHw84pUHU3fVHj7TSVy0=\n\n— rekor.sigstore.dev wNI9ajBEAiBIowx1POsWydf7F2tZj7huPfFBNngo87WIw2PyWTu5SgIgTxuNk/AFSdY2DjdM+2NodtymfDr0QydRAh8UO9ab8WU=\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiNGE5NTU0MjA2YTk2OTA1ODgyOGU5MTNmOTcwZmE4MGI4NWE4ZDliN2RjZTJiZmI2NGM5Njc1YTY1ZTFjYTVlNyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6Ijc2MTgxNmVmNmFjMDNhOWZhN2JmMDQxZmQ4ZjNhZmI1ODRhZTQxYjRlYzNjMmZmYzVmOTkwY2MwMmU3OTcxMGIifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVZQ0lRRHFnbnY5MVpUM0J6clQ4UHk4bHpneStZL28xa1ZqNTFkeUIxWXI4Nlc2RFFJaEFOaWNyNm9hUjR4VkhSNWRtYUpLQ3p6NCttcUFwNUcyREpsaTFMTW9BSVpQIiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoS2VrTkRRbkY1WjBGM1NVSkJaMGxWUzBaaGNVWTRiRkZ6YnpoNU5FMHlUa2RHZFRKV05rWnRaVWxOZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwNXFSWGROVkdzd1RucEpNVmRvWTA1TmFsRjNUbXBGZDAxVWF6Rk9la2t4VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVmFWM2d3VFRab2VrRm1iRFp4YkhKc1p5dElRVmgzVkVWdFlVVk9TR1p5ZWxRelNsSUtkSE15VlVkVmNrWjFaV3R3YUhaYVNrOXdjRTh5U2xCSFVYVm1NR1ZVVDNsTGFrdzJPVFpzWW1aNmRFRllNRFJRYVhGUFEwSmpjM2RuWjFoSVRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVmFWRnBLQ2tSMFJrcE5Wblo2Ums4MWRUQlZhalpwYmpFd1QxaHJkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMlJSV1VSV1VqQlNRVkZJTDBKSGMzZGhXVnB1WVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBURE5TZVZsWGJITmlNbHBwWVZoU2VncE1NMEkxWTBkcmRGbFlVakJhV0U0d1dWaFNjR0l5TkhSaVZ6bHJXbGQ0ZWt4NU5XNWhXRkp2WkZkSmRtUXlPWGxoTWxwellqTmtla3d6U214aVIxWm9DbU15VlhWbFZ6RnpVVWhLYkZwdVRYWmtSMFp1WTNrNU1rMUROSGRNYWxKb1RXcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1lLVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSlZSME5wYzBkQlVWRkNaemM0ZHdwQlVVbEZRak5LYkdKSFZtaGpNbFYzVG1kWlMwdDNXVUpDUVVkRWRucEJRa0YzVVc5TlZGVjZXWHBzYUUweVdURlpla1V4VFcxV2JVNVVVWGRPVjA1cENsbFVTWGxPZWxrelRtcFZlazVFV1RGUFIwWnFXa2RWZVU1NlFWWkNaMjl5UW1kRlJVRlpUeTlOUVVWRlFrRmtlVnBYZUd4WldFNXNUVVJGUjBOcGMwY0tRVkZSUW1jM09IZEJVVlZGU1ROU2VWbFhiSE5pTWxwcFlWaFNla3d6UWpWalIydDBXVmhTTUZwWVRqQlpXRkp3WWpJMGRHSlhPV3RhVjNoNlRVTkJSd3BEYVhOSFFWRlJRbWMzT0hkQlVWbEZSVzVLYkZwdVRYWmtSMFp1WTNrNU1rMUROSGRNYWxKb1RXcEJOMEpuYjNKQ1owVkZRVmxQTDAxQlJVbENRekJOQ2tzeWFEQmtTRUo2VDJrNGRtUkhPWEphVnpSMVdWZE9NR0ZYT1hWamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcVlqSXdkMlIzV1VzS1MzZFpRa0pCUjBSMmVrRkNRMUZTY0VSSFpHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaa1NFcG9ZVmQ0ZGxwdFNuQmtTRTEyWTBoc2R3cGhVekZvWkVoU2JHTXpVbWhrUjJ4Mllta3hkR0l5VW14aVNFMTJURzFrY0dSSGFERlphVGt6WWpOS2NscHRlSFprTTAxMlkyMVdjMXBYUm5wYVV6VTFDbUpYZUVGamJWWnRZM2s1TUZsWFpIcE1NMWwzVEdwQmRVNUhSWGxOUkdkSFEybHpSMEZSVVVKbk56aDNRVkZ2UlV0bmQyOU5WRlY2V1hwc2FFMHlXVEVLV1hwRk1VMXRWbTFPVkZGM1RsZE9hVmxVU1hsT2Vsa3pUbXBWZWs1RVdURlBSMFpxV2tkVmVVNTZRV1JDWjI5eVFtZEZSVUZaVHk5TlFVVk1Ra0U0VFFwRVYyUndaRWRvTVZscE1XOWlNMDR3V2xkUmQxSm5XVXRMZDFsQ1FrRkhSSFo2UVVKRVFWRTBSRVJhYjJSSVVuZGplbTkyVERKa2NHUkhhREZaYVRWcUNtSXlNSFprU0Vwb1lWZDRkbHB0U25Ca1NFMTJZMGhzZDJGVE1XaGtTRkpzWXpOU2FHUkhiSFppYVRGMFlqSlNiR0pJVFhkUFFWbExTM2RaUWtKQlIwUUtkbnBCUWtSUlVYRkVRMmQ0VGxST2FrOVhSWHBhYWxacVRWUlZlVnBYV1RGT1JFRXhXVEpLYUUxcVNUTk9hbU15VGxSTk1FNXFWVFJaVjA1cldsUkpNd3BOUTBsSFEybHpSMEZSVVVKbk56aDNRVkUwUlVaQmQxTmpiVlp0WTNrNU1GbFhaSHBNTTFsM1RHcEJkVTVIUlhsTlFtdEhRMmx6UjBGUlVVSm5OemgzQ2tGUk9FVkRkM2RLVG5wamVVMXFVVE5PUkVsNlRVTTBSME5wYzBkQlVWRkNaemM0ZDBGU1FVVkpRWGRsWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hVS1dUSTVkRXd6VW5sWlYyeHpZakphYVdGWVVucE5RbU5IUTJselIwRlJVVUpuTnpoM1FWSkZSVU5SZDBoTmFrMTRUa1JSZVUxNlFqTkNaMjl5UW1kRlJRcEJXVTh2VFVGRlUwSkhhMDFhTW1nd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemt3WTIxR2NHSkhPVzFaYld3d1kzazVkMlZZUW5CTVYwWXdDbVJIVm5wa1IwWXdZVmM1ZFV4WE1YWmFSMVp6WTNrNGRWb3liREJoU0ZacFRETmtkbU50ZEcxaVJ6a3pZM2s1ZVZwWGVHeFpXRTVzVEc1c2RHSkZRbmtLV2xkYWVrd3pVbWhhTTAxMlpHcEJkVTFETkRCWlZFbDNUMEZaUzB0M1dVSkNRVWRFZG5wQlFrVjNVWEZFUTJkNFRsUk9hazlYUlhwYWFsWnFUVlJWZVFwYVYxa3hUa1JCTVZreVNtaE5ha2t6VG1wak1rNVVUVEJPYWxVMFdWZE9hMXBVU1ROTlFtTkhRMmx6UjBGUlVVSm5OemgzUVZKUlJVTlJkMGhqYlZaekNscFhSbnBhVkVKd1FtZHZja0puUlVWQldVOHZUVUZGVmtKR2MwMVhWMmd3WkVoQ2VrOXBPSFphTW13d1lVaFdhVXh0VG5aaVV6a3dZMjFHY0dKSE9XMEtXVzFzTUdONU9YZGxXRUp3VEZkR01HUkhWbnBrUjBZd1lWYzVkVXhYTVhaYVIxWnpZM2s1YUZrelVuQmlNalY2VEROS01XSnVUWFpQVkZFeFRrUlZOUXBOUkdkM1RVTTVhR1JJVW14aVdFSXdZM2s0ZUUxQ1dVZERhWE5IUVZGUlFtYzNPSGRCVWxsRlEwRjNSMk5JVm1saVIyeHFUVWxIUzBKbmIzSkNaMFZGQ2tGa1dqVkJaMUZEUWtoM1JXVm5RalJCU0ZsQk0xUXdkMkZ6WWtoRlZFcHFSMUkwWTIxWFl6TkJjVXBMV0hKcVpWQkxNeTlvTkhCNVowTTRjRGR2TkVFS1FVRkhVVUUzUkhNM2QwRkJRa0ZOUVZKNlFrWkJhVUZZUVVkV04wUk1UM1Z6ZGpsTFpFeFZiVmsyZGxSd01rMU5aVFJUZERsT1ZVOW9SWEF2WlZoYVNRcDNkMGxvUVV0TFdXbzFSR1pZT1d4MlNsVklRbk55TDBGMFJVbGxTbGx4VTJWS05rMHpRMHRRVlRFNFJsSjRXSE5OUVc5SFEwTnhSMU5OTkRsQ1FVMUVDa0V5YTBGTlIxbERUVkZFZVU0MWJHaFNRM3AxUjJ4eVowVktVbkJIY0djMWFtUndZVlJKY0dsQ2RYTXdkbXRCUjJabWVsQmFjamxUYWt0M1pVZHZVbFVLZEV4dVpuaEJTalpLYURSRFRWRkRXVVZYUVdOWlJWWlBVRVZCUTJVclRVMUlNRUpRY25Kc1VrMXVabTl2ZFc0NU4xQnRkVkV5TlV4M1psWnBOVkEwT0FwR2IzUnRPRWhhTUZacFdFaFZXa1U5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0="}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicHlwaV9hdHRlc3RhdGlvbl9tb2RlbHMtMC4wLjRhMi50YXIuZ3oiLCJkaWdlc3QiOnsic2hhMjU2IjoiYzk3MDljZTZmZDViNjdiNTliNGEyODc1OGNmMTRkM2Y0MTE4MDNjNGI4OWI2MDY4YjFmMWE4ZTRlZTk0YzhlZiJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRpb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOnt9fQ==","signature":"MEYCIQDqgnv91ZT3BzrT8Py8lzgy+Y/o1kVj51dyB1Yr86W6DQIhANicr6oaR4xVHR5dmaJKCzz4+mqAp5G2DJli1LMoAIZP"}}

test/test_cli.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
get_identity_token,
2222
main,
2323
)
24-
from pypi_attestations._impl import Attestation, AttestationError, Distribution
24+
from pypi_attestations._impl import Attestation, AttestationError, ConversionError, Distribution
2525

2626
ONLINE_TESTS = (
2727
"CI" in os.environ or "TEST_INTERACTIVE" in os.environ
@@ -46,6 +46,9 @@
4646
pypi_sdist_path = _ASSETS / pypi_sdist_filename
4747
pypi_sdist_provenance_path = _ASSETS / f"{pypi_sdist_filename}.provenance"
4848

49+
sigstore_bundle_path = _ASSETS / "pypi_attestation_models-0.0.4a2.tar.gz.sigstore"
50+
converted_sigstore_bundle_path = _ASSETS / "pypi_attestation_models-0.0.4a2.tar.gz.attestation"
51+
4952

5053
def run_main_with_command(cmd: list[str]) -> None:
5154
"""Helper method to run the main function with a given command."""
@@ -812,3 +815,101 @@ def test_verify_pypi_command_local_invalid_provenance(
812815
)
813816

814817
assert "Invalid provenance" in caplog.text
818+
819+
820+
def test_convert_command(caplog: pytest.LogCaptureFixture) -> None:
821+
with tempfile.TemporaryDirectory() as tmpdir:
822+
output_attestation_path = Path(tmpdir) / "temp.attestation"
823+
run_main_with_command(
824+
[
825+
"convert",
826+
"--output-file",
827+
output_attestation_path.as_posix(),
828+
sigstore_bundle_path.as_posix(),
829+
]
830+
)
831+
assert output_attestation_path.is_file()
832+
converted_attestation = Attestation.model_validate_json(
833+
output_attestation_path.read_bytes()
834+
)
835+
known_good_attestation = Attestation.model_validate_json(
836+
converted_sigstore_bundle_path.read_bytes()
837+
)
838+
839+
assert converted_attestation.version == 1
840+
assert (
841+
converted_attestation.verification_material
842+
== known_good_attestation.verification_material
843+
)
844+
assert converted_attestation.envelope == known_good_attestation.envelope
845+
846+
847+
def test_convert_command_invalid_bundle(caplog: pytest.LogCaptureFixture) -> None:
848+
with tempfile.NamedTemporaryFile(suffix=".sigstore") as f:
849+
f.write(b"not a valid bundle")
850+
f.flush()
851+
with pytest.raises(SystemExit):
852+
run_main_with_command(
853+
[
854+
"convert",
855+
"--output-file",
856+
"temp.attestation",
857+
f.name,
858+
]
859+
)
860+
861+
assert not Path("temp.attestation").exists()
862+
assert "Invalid Sigstore bundle" in caplog.text
863+
864+
865+
def test_convert_command_conversion_error(
866+
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
867+
) -> None:
868+
monkeypatch.setattr(
869+
pypi_attestations._cli, "Attestation", stub(from_bundle=raiser(ConversionError))
870+
)
871+
872+
with pytest.raises(SystemExit):
873+
run_main_with_command(
874+
[
875+
"convert",
876+
"--output-file",
877+
"temp.attestation",
878+
sigstore_bundle_path.as_posix(),
879+
]
880+
)
881+
882+
assert not Path("temp.attestation").exists()
883+
assert "Failed to convert Sigstore bundle" in caplog.text
884+
885+
886+
def test_convert_command_nonexistent_bundle(caplog: pytest.LogCaptureFixture) -> None:
887+
with pytest.raises(SystemExit):
888+
run_main_with_command(
889+
[
890+
"convert",
891+
"--output-file",
892+
"temp.attestation",
893+
"temp.sigstore",
894+
]
895+
)
896+
897+
assert not Path("temp.attestation").exists()
898+
assert "Bundle file does not exist" in caplog.text
899+
900+
901+
def test_convert_command_existent_output_file(caplog: pytest.LogCaptureFixture) -> None:
902+
with tempfile.NamedTemporaryFile(suffix=".attestation") as f:
903+
output_attestation_path = Path(f.name)
904+
assert output_attestation_path.exists()
905+
with pytest.raises(SystemExit):
906+
run_main_with_command(
907+
[
908+
"convert",
909+
"--output-file",
910+
output_attestation_path.as_posix(),
911+
sigstore_bundle_path.as_posix(),
912+
]
913+
)
914+
915+
assert "Output file already exists" in caplog.text

0 commit comments

Comments
 (0)