Skip to content

Commit 26ba400

Browse files
committed
feat: add local artifact support
Signed-off-by: Ben Selwyn-Smith <[email protected]>
1 parent 8df2f50 commit 26ba400

File tree

9 files changed

+281
-75
lines changed

9 files changed

+281
-75
lines changed

src/macaron/__main__.py

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os
1010
import sys
1111
from importlib import metadata as importlib_metadata
12+
from typing import Any
1213

1314
from jinja2 import Environment, FileSystemLoader, select_autoescape
1415
from packageurl import PackageURL
@@ -96,6 +97,12 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
9697

9798
global_config.local_maven_repo = user_provided_local_maven_repo
9899

100+
# Set local artifact path.
101+
if analyzer_single_args.local_artifact_path is not None and os.path.isfile(
102+
analyzer_single_args.local_artifact_path
103+
):
104+
global_config.local_artifact_path = analyzer_single_args.local_artifact_path
105+
99106
analyzer = Analyzer(global_config.output_path, global_config.build_log_path)
100107

101108
# Initiate reporters.
@@ -118,7 +125,6 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None
118125
analyzer.reporters.append(HTMLReporter())
119126
analyzer.reporters.append(JSONReporter())
120127

121-
run_config = {}
122128
repo_path = analyzer_single_args.repo_path
123129
purl = analyzer_single_args.package_url
124130
branch = analyzer_single_args.branch
@@ -370,8 +376,25 @@ def main(argv: list[str] | None = None) -> None:
370376
# Add sub parsers for each action.
371377
sub_parser = main_parser.add_subparsers(dest="action", help="Run macaron <action> --help for help")
372378

379+
# Dump the default values.
380+
sub_parser.add_parser(name="dump-defaults", description="Dumps the defaults.ini file to the output directory.")
381+
382+
# Add the sub parser commands.
383+
_add_analyzer_parser(sub_parser)
384+
_add_verify_policy_parser(sub_parser)
385+
_add_find_source_parser(sub_parser)
386+
387+
# Perform parsing.
388+
args = parse_arguments(main_parser, argv)
389+
390+
# Perform actions.
391+
perform_action(args)
392+
393+
394+
def _add_analyzer_parser(parser: Any) -> None:
395+
"""Add the analyzer commands to the parser."""
373396
# Use Macaron to analyze one single repository.
374-
single_analyze_parser = sub_parser.add_parser(name="analyze")
397+
single_analyze_parser = parser.add_parser(name="analyze")
375398

376399
single_analyze_parser.add_argument(
377400
"-sbom",
@@ -390,7 +413,7 @@ def main(argv: list[str] | None = None) -> None:
390413
"--repo-path",
391414
required=False,
392415
type=str,
393-
help=("The path to the repository, can be local or remote"),
416+
help="The path to the repository, can be local or remote",
394417
)
395418

396419
single_analyze_parser.add_argument(
@@ -411,7 +434,7 @@ def main(argv: list[str] | None = None) -> None:
411434
required=False,
412435
type=str,
413436
default="",
414-
help=("The branch of the repository that we want to checkout. If not set, Macaron will use the default branch"),
437+
help="The branch of the repository that we want to checkout. If not set, Macaron will use the default branch",
415438
)
416439

417440
single_analyze_parser.add_argument(
@@ -430,14 +453,14 @@ def main(argv: list[str] | None = None) -> None:
430453
"-pe",
431454
"--provenance-expectation",
432455
required=False,
433-
help=("The path to provenance expectation file or directory."),
456+
help="The path to provenance expectation file or directory.",
434457
)
435458

436459
single_analyze_parser.add_argument(
437460
"-pf",
438461
"--provenance-file",
439462
required=False,
440-
help=("The path to the provenance file in in-toto format."),
463+
help="The path to the provenance file in in-toto format.",
441464
)
442465

443466
single_analyze_parser.add_argument(
@@ -456,7 +479,7 @@ def main(argv: list[str] | None = None) -> None:
456479
required=False,
457480
type=str,
458481
default="",
459-
help=("The path to the Jinja2 html template (please make sure to use .html or .j2 extensions)."),
482+
help="The path to the Jinja2 html template (please make sure to use .html or .j2 extensions).",
460483
)
461484

462485
single_analyze_parser.add_argument(
@@ -472,44 +495,56 @@ def main(argv: list[str] | None = None) -> None:
472495
"--local-maven-repo",
473496
required=False,
474497
help=(
475-
"The path to the local .m2 directory. If this option is not used, Macaron will use the default location at $HOME/.m2"
498+
"The path to the local .m2 directory. "
499+
"If this option is not used, Macaron will use the default location at $HOME/.m2"
476500
),
477501
)
478502

479503
single_analyze_parser.add_argument(
480504
"--force-analyze-source",
481505
required=False,
482506
action="store_true",
483-
help=("Forces PyPI sourcecode analysis to run regardless of other heuristic results."),
507+
help="Forces PyPI sourcecode analysis to run regardless of other heuristic results.",
484508
)
485509

486510
single_analyze_parser.add_argument(
487511
"--verify-provenance",
488512
required=False,
489513
action="store_true",
490-
help=("Allow the analysis to attempt to verify provenance files as part of its normal operations."),
514+
help="Allow the analysis to attempt to verify provenance files as part of its normal operations.",
491515
)
492516

493-
# Dump the default values.
494-
sub_parser.add_parser(name="dump-defaults", description="Dumps the defaults.ini file to the output directory.")
517+
single_analyze_parser.add_argument(
518+
"-ap",
519+
"--local-artifact-path",
520+
required=False,
521+
type=str,
522+
help="The path to the local artifact file that should match the target software component being analyzed.",
523+
)
495524

525+
526+
def _add_verify_policy_parser(parser: Any) -> None:
527+
"""Add the verify policy commands parser."""
496528
# Verify the Datalog policy.
497-
vp_parser = sub_parser.add_parser(name="verify-policy")
529+
vp_parser = parser.add_parser(name="verify-policy")
498530
vp_group = vp_parser.add_mutually_exclusive_group(required=True)
499531

500532
vp_parser.add_argument("-d", "--database", required=True, type=str, help="Path to the database.")
501533
vp_group.add_argument("-f", "--file", type=str, help="Path to the Datalog policy.")
502534
vp_group.add_argument("-s", "--show-prelude", action="store_true", help="Show policy prelude.")
503535

536+
537+
def _add_find_source_parser(parser: Any) -> None:
538+
"""Add the find source commands parser."""
504539
# Find the repo and commit of a passed PURL, or the commit of a passed PURL and repo.
505-
find_parser = sub_parser.add_parser(name="find-source")
540+
find_parser = parser.add_parser(name="find-source")
506541

507542
find_parser.add_argument(
508543
"-purl",
509544
"--package-url",
510545
required=True,
511546
type=str,
512-
help=("The PURL string to perform repository and commit finding for."),
547+
help="The PURL string to perform repository and commit finding for.",
513548
)
514549

515550
find_parser.add_argument(
@@ -523,10 +558,26 @@ def main(argv: list[str] | None = None) -> None:
523558
),
524559
)
525560

526-
args = main_parser.parse_args(argv)
561+
562+
def parse_arguments(parser: argparse.ArgumentParser, argv: list[str] | None) -> argparse.Namespace:
563+
"""Parse the arguments of the argument parser.
564+
565+
Parameters
566+
----------
567+
parser: argparse.ArgumentParser
568+
The parser to use.
569+
argv: list[str]
570+
The list of arguments for the parser to parse.
571+
572+
Returns
573+
-------
574+
argparse.Namespace
575+
The results of the argument parsing.
576+
"""
577+
args = parser.parse_args(argv)
527578

528579
if not args.action:
529-
main_parser.print_help()
580+
parser.print_help()
530581
sys.exit(os.EX_USAGE)
531582

532583
if args.verbose:
@@ -587,7 +638,7 @@ def main(argv: list[str] | None = None) -> None:
587638
logger.error("Exiting because the defaults configuration could not be loaded.")
588639
sys.exit(os.EX_NOINPUT)
589640

590-
perform_action(args)
641+
return args
591642

592643

593644
def _get_token_from_dict_or_env(token: str, token_dict: dict[str, str]) -> str:

src/macaron/artifact/local_artifact.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,33 @@ def get_local_artifact_dirs(
253253
raise LocalArtifactFinderError(f"Unsupported PURL type {purl_type}")
254254

255255

256-
def get_local_artifact_hash(purl: PackageURL, artifact_dirs: list[str]) -> str | None:
257-
"""Compute the hash of the local artifact.
256+
def get_artifact_hash_from_file(artifact_path: str) -> str | None:
257+
"""Compute the hash of the passed artifact.
258+
259+
Parameters
260+
----------
261+
artifact_path: str
262+
The path to the artifact.
263+
264+
Returns
265+
-------
266+
str | None
267+
The artifact hash, or None if it could not be computed.
268+
"""
269+
if not os.path.isfile(artifact_path):
270+
return None
271+
272+
with open(artifact_path, "rb") as file:
273+
try:
274+
hash_result = hashlib.file_digest(file, "sha256")
275+
return hash_result.hexdigest()
276+
except ValueError as error:
277+
logger.debug("Error while hashing file: %s", error)
278+
return None
279+
280+
281+
def get_artifact_hash_from_directory(purl: PackageURL, artifact_dirs: list[str]) -> tuple[str | None, str | None]:
282+
"""Compute the hash of a local artifact found within the passed directories.
258283
259284
Parameters
260285
----------
@@ -265,16 +290,16 @@ def get_local_artifact_hash(purl: PackageURL, artifact_dirs: list[str]) -> str |
265290
266291
Returns
267292
-------
268-
str | None
269-
The hash, or None if not found.
293+
tuple[str | None, str | None]
294+
The hash of, and path to, the artifact; or None if no artifact can be found locally or remotely.
270295
"""
271296
if not artifact_dirs:
272297
logger.debug("No artifact directories provided.")
273-
return None
298+
return None, None
274299

275300
if not purl.version:
276301
logger.debug("PURL is missing version.")
277-
return None
302+
return None, None
278303

279304
artifact_target = None
280305
if purl.type == "maven":
@@ -286,20 +311,14 @@ def get_local_artifact_hash(purl: PackageURL, artifact_dirs: list[str]) -> str |
286311

287312
if not artifact_target:
288313
logger.debug("PURL type not supported: %s", purl.type)
289-
return None
314+
return None, None
290315

291316
for artifact_dir in artifact_dirs:
292-
full_path = os.path.join(artifact_dir, artifact_target)
293-
if not os.path.exists(full_path):
317+
if not os.path.isdir(artifact_dir):
294318
continue
319+
possible_artifact_path = os.path.join(artifact_dir, artifact_target)
320+
artifact_hash = get_artifact_hash_from_file(possible_artifact_path)
321+
if artifact_hash:
322+
return artifact_hash, possible_artifact_path
295323

296-
with open(full_path, "rb") as file:
297-
try:
298-
hash_result = hashlib.file_digest(file, "sha256")
299-
except ValueError as error:
300-
logger.debug("Error while hashing file: %s", error)
301-
continue
302-
303-
return hash_result.hexdigest()
304-
305-
return None
324+
return None, None

src/macaron/config/global_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ class GlobalConfig:
4949
#: The path to the local .m2 Maven repository. This attribute is None if there is no available .m2 directory.
5050
local_maven_repo: str | None = None
5151

52+
#: The path to a local artifact file that can be used for analysis.
53+
local_artifact_path: str | None = None
54+
5255
def load(
5356
self,
5457
macaron_path: str,

0 commit comments

Comments
 (0)