diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index 500d97c2f..cf53dd53e 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -57,6 +57,7 @@ parent_limit = 10 # Disables repo finding for specific artifacts based on their group and artifact IDs. Format: {groupId}:{artifactId} # E.g. com.oracle.coherence.ce:coherence artifact_ignore_list = +use_database = True [git] # The list of allowed git hosts. diff --git a/src/macaron/database/database_manager.py b/src/macaron/database/database_manager.py index b375b512a..d5a4701d3 100644 --- a/src/macaron/database/database_manager.py +++ b/src/macaron/database/database_manager.py @@ -96,9 +96,9 @@ def insert(self, table: Table, values: dict) -> None: Parameters ---------- table: Table - The Table to insert to + The Table to insert to. values: dict - The mapping from column names to values to insert into the Table + The mapping from column names to values to insert into the Table. """ try: self.execute(insert(table).values(**values)) @@ -112,12 +112,29 @@ def execute(self, query: Any) -> None: Parameters ---------- query: Any - The SQLalchemy query to execute + The SQLAlchemy query to execute. """ with self.engine.connect() as conn: conn.execute(query) conn.commit() + def execute_and_return(self, query: Any) -> sqlalchemy.engine.cursor.CursorResult: + """ + Execute a SQLAlchemy core api query using a short-lived engine connection and returns the result. + + Parameters + ---------- + query: Any + The SQLAlchemy query to execute. + + Returns + ------- + Any : + The result of the query. + """ + with self.engine.connect() as conn: + return conn.execute(query) + def create_tables(self) -> None: """ Automatically create views for all tables known to _base.metadata. diff --git a/src/macaron/database/table_definitions.py b/src/macaron/database/table_definitions.py index d8b63d95c..fb4030879 100644 --- a/src/macaron/database/table_definitions.py +++ b/src/macaron/database/table_definitions.py @@ -77,6 +77,8 @@ class RepositoryTable(ORMBase): release_tag: Mapped[str] = mapped_column(String, nullable=True) commit_sha: Mapped[str] = mapped_column(String, nullable=False) commit_date: Mapped[str] = mapped_column(String, nullable=False) + namespace: Mapped[str] = mapped_column(String, nullable=True) + name: Mapped[str] = mapped_column(String, nullable=True) class SLSALevelTable(ORMBase): diff --git a/src/macaron/dependency_analyzer/cyclonedx.py b/src/macaron/dependency_analyzer/cyclonedx.py index 77a847717..604658a84 100644 --- a/src/macaron/dependency_analyzer/cyclonedx.py +++ b/src/macaron/dependency_analyzer/cyclonedx.py @@ -12,6 +12,7 @@ from macaron.config.defaults import defaults from macaron.config.global_config import global_config +from macaron.database.database_manager import DatabaseManager from macaron.dependency_analyzer.dependency_resolver import DependencyAnalyzer, DependencyInfo from macaron.errors import MacaronError from macaron.output_reporter.scm import SCMStatus @@ -139,12 +140,14 @@ def get_dep_components( def convert_components_to_artifacts( - components: Iterable[dict], root_component: Optional[dict | None] = None + db_man: DatabaseManager | None, components: Iterable[dict], root_component: Optional[dict | None] = None ) -> dict[str, DependencyInfo]: """Convert CycloneDX components using internal artifact representation. Parameters ---------- + db_man : DatabaseManager + The database manager for accessing the database (optional). components : list[dict] The dependency components. root_component: Optional[dict|None] @@ -196,7 +199,12 @@ def convert_components_to_artifacts( ) DependencyAnalyzer.add_latest_version( - item=item, key=key, all_versions=all_versions, latest_deps=latest_deps, url_to_artifact=url_to_artifact + db_man=db_man, + item=item, + key=key, + all_versions=all_versions, + latest_deps=latest_deps, + url_to_artifact=url_to_artifact, ) except KeyError as error: logger.debug(error) @@ -210,11 +218,13 @@ def convert_components_to_artifacts( return latest_deps -def get_deps_from_sbom(sbom_path: str | Path) -> dict[str, DependencyInfo]: +def get_deps_from_sbom(db_man: DatabaseManager | None, sbom_path: str | Path) -> dict[str, DependencyInfo]: """Get the dependencies from a provided SBOM. Parameters ---------- + db_man : DatabaseManager + The database manager for accessing the database (optional). sbom_path : str | Path The path to the SBOM file. @@ -223,6 +233,7 @@ def get_deps_from_sbom(sbom_path: str | Path) -> dict[str, DependencyInfo]: A dictionary where dependency artifacts are grouped based on "artifactId:groupId". """ return convert_components_to_artifacts( + db_man, get_dep_components( root_bom_path=Path(sbom_path), recursive=defaults.getboolean( @@ -230,5 +241,5 @@ def get_deps_from_sbom(sbom_path: str | Path) -> dict[str, DependencyInfo]: "recursive", fallback=False, ), - ) + ), ) diff --git a/src/macaron/dependency_analyzer/cyclonedx_gradle.py b/src/macaron/dependency_analyzer/cyclonedx_gradle.py index 76f9c0246..3a884cf0f 100644 --- a/src/macaron/dependency_analyzer/cyclonedx_gradle.py +++ b/src/macaron/dependency_analyzer/cyclonedx_gradle.py @@ -87,7 +87,7 @@ def collect_dependencies(self, dir_path: str) -> dict[str, DependencyInfo]: fallback=False, ), ) - return convert_components_to_artifacts(components, root_component) + return convert_components_to_artifacts(None, components, root_component) def remove_sboms(self, dir_path: str) -> bool: """Remove all the SBOM files in the provided directory recursively. diff --git a/src/macaron/dependency_analyzer/cyclonedx_mvn.py b/src/macaron/dependency_analyzer/cyclonedx_mvn.py index 73ca509d2..0732bc130 100644 --- a/src/macaron/dependency_analyzer/cyclonedx_mvn.py +++ b/src/macaron/dependency_analyzer/cyclonedx_mvn.py @@ -89,7 +89,7 @@ def collect_dependencies(self, dir_path: str) -> dict[str, DependencyInfo]: fallback=False, ), ) - return convert_components_to_artifacts(components, root_component) + return convert_components_to_artifacts(None, components, root_component) def remove_sboms(self, dir_path: str) -> bool: """Remove all the SBOM files in the provided directory recursively. diff --git a/src/macaron/dependency_analyzer/dependency_resolver.py b/src/macaron/dependency_analyzer/dependency_resolver.py index 9fa742eb0..0c67b60ad 100644 --- a/src/macaron/dependency_analyzer/dependency_resolver.py +++ b/src/macaron/dependency_analyzer/dependency_resolver.py @@ -9,10 +9,13 @@ from enum import Enum from typing import TypedDict +import sqlalchemy from packaging import version from macaron.config.defaults import defaults from macaron.config.target_config import Configuration +from macaron.database.database_manager import DatabaseManager +from macaron.database.table_definitions import RepositoryTable from macaron.dependency_analyzer.java_repo_finder import find_java_repo from macaron.errors import MacaronError from macaron.output_reporter.scm import SCMStatus @@ -111,6 +114,7 @@ def get_cmd(self) -> list: @staticmethod def add_latest_version( + db_man: DatabaseManager | None, item: DependencyInfo, key: str, all_versions: dict[str, list[DependencyInfo]], @@ -121,6 +125,8 @@ def add_latest_version( Parameters ---------- + db_man : DatabaseManager | None + The database manager for accessing the database (optional). item : DependencyInfo The dictionary containing info about the dependency to be added. key : str @@ -133,7 +139,7 @@ def add_latest_version( Used to detect artifacts that have similar repos. """ if defaults.getboolean("repofinder.java", "find_repos"): - DependencyAnalyzer._find_repo(item) + DependencyAnalyzer._find_repo(db_man, item) # Check if the URL is already seen for a different artifact. if item["url"] != "": @@ -173,16 +179,29 @@ def add_latest_version( logger.error("Could not parse dependency version number: %s", error) @staticmethod - def _find_repo(item: DependencyInfo) -> None: + def _find_repo(db_man: DatabaseManager | None, item: DependencyInfo) -> None: """Find the repo for the current item, if the criteria are met.""" if item["url"] != "" or item["version"] == "unspecified" or not item["group"] or not item["name"]: logger.debug("Item URL already exists, or item is missing information: %s", item) return - gav = f"{item['group']}:{item['name']}:{item['version']}" + artifact = f"{item['group']}:{item['name']}" if f"{item['group']}:{item['name']}" in defaults.get_list("repofinder.java", "artifact_ignore_list"): - logger.debug("Skipping GAV: %s", gav) + logger.debug("Skipping artifact: %s", artifact) return + if db_man and defaults.getboolean("repofinder.java", "use_database"): + # Perform database lookup + query = sqlalchemy.select(RepositoryTable.remote_path).where( + RepositoryTable.namespace == item["group"], RepositoryTable.name == item["name"] + ) + result: sqlalchemy.engine.cursor.CursorResult = db_man.execute_and_return(query) + row = result.first() + if row and row.remote_path: + logger.debug("Found database url: %s for artifact: %s", row.remote_path, artifact) + item["url"] = row.remote_path + return + logger.debug("No database url found for GAV: %s", artifact) + urls = find_java_repo( item["group"], item["name"], @@ -191,7 +210,7 @@ def _find_repo(item: DependencyInfo) -> None: ) item["url"] = DependencyAnalyzer.find_valid_url(list(urls)) if item["url"] == "": - logger.debug("Failed to find url for GAV: %s", gav) + logger.debug("Failed to find url for artifact: %s", artifact) @staticmethod def find_valid_url(urls: Iterable[str]) -> str: diff --git a/src/macaron/policy_engine/prelude/aggregate_rules.dl b/src/macaron/policy_engine/prelude/aggregate_rules.dl index 64c1a074c..d7717af46 100644 --- a/src/macaron/policy_engine/prelude/aggregate_rules.dl +++ b/src/macaron/policy_engine/prelude/aggregate_rules.dl @@ -35,7 +35,7 @@ agg_levels(n+1) :- n <= 4, agg_levels(n). * Everything has a repository and uses a scripted build service. */ aggregate_level_requirement(1, repo) :- - repository(repo, _,_,_,_,_,_), + repository(repo, _,_,_,_,_,_,_,_), check_passed(repo, "mcn_build_service_1"), check_passed(repo, "mcn_version_control_system_1"). @@ -44,7 +44,7 @@ aggregate_level_requirement(1, repo) :- * The build is verifiably automated and deployable. */ aggregate_level_requirement(2, repo) :- - repository(repo, _,_,_,_,_,_), + repository(repo, _,_,_,_,_,_,_,_), aggregate_level_requirement(1, repo), check_passed(repo, "mcn_build_script_1"), check_passed(repo, "mcn_build_service_1"), @@ -55,7 +55,7 @@ aggregate_level_requirement(2, repo) :- * provenance information. */ aggregate_level_requirement(3, repo) :- - repository(repo, _,_,_,_,_,_), + repository(repo, _,_,_,_,_,_,_,_), check_passed(repo, "mcn_provenance_level_three_1"), aggregate_level_requirement(2, repo). @@ -63,7 +63,7 @@ aggregate_level_requirement(3, repo) :- * The release provenance passes verification. */ aggregate_level_requirement(4, repo) :- - repository(repo, _,_,_,_,_,_), + repository(repo, _,_,_,_,_,_,_,_), aggregate_level_requirement(3, repo), check_passed(repo, "mcn_provenance_level_three_1"), check_passed(repo, "mcn_trusted_builder_level_three_1"), @@ -94,9 +94,9 @@ aggregate_level_min_dependency_level(level, repo) <= aggregate_level_min_depende /** * The aggregate level for each repository that does not have any dependencies asserts the requirements are met. */ -aggregate_level(0, repo) :- repository(repo, _,_,_,_,_,_). +aggregate_level(0, repo) :- repository(repo, _,_,_,_,_,_,_,_). aggregate_level(level, repo) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), agg_levels(level), // this level's requirements aggregate_level_requirement(level, repo), @@ -108,7 +108,7 @@ aggregate_level(level, repo) :- * reach the required minimum level. */ aggregate_level(level, repo) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), agg_levels(level), // this level's requirements aggregate_level_requirement(level, repo), @@ -130,17 +130,17 @@ aggregate_level(level, repo) <= aggregate_level(higher_level, repo) :- meets_aggregate_level(level, repo) :- aggregate_level(real_level, repo), agg_levels(level), level <= real_level. Policy("aggregate_level_4", repo, reponame) :- - repository(repo, reponame,_,_,_,_,_), + repository(repo, reponame,_,_,_,_,_,_,_), meets_aggregate_level(4, repo). Policy("aggregate_level_3", repo, reponame) :- - repository(repo, reponame,_,_,_,_,_), + repository(repo, reponame,_,_,_,_,_,_,_), meets_aggregate_level(3, repo). Policy("aggregate_level_2", repo, reponame) :- - repository(repo, reponame,_,_,_,_,_), + repository(repo, reponame,_,_,_,_,_,_,_), meets_aggregate_level(2, repo). Policy("aggregate_level_1", repo, reponame) :- - repository(repo, reponame,_,_,_,_,_), + repository(repo, reponame,_,_,_,_,_,_,_), meets_aggregate_level(1, repo). diff --git a/src/macaron/policy_engine/prelude/helper_rules.dl b/src/macaron/policy_engine/prelude/helper_rules.dl index a8c922b54..7457f8f8c 100644 --- a/src/macaron/policy_engine/prelude/helper_rules.dl +++ b/src/macaron/policy_engine/prelude/helper_rules.dl @@ -25,7 +25,7 @@ is_check(check_name) :- check_result(_, check_name, _, _, _). * This fact exists iff a repository is hosted on a trusted public platform. */ .decl not_self_hosted_git(repo:number, message:symbol) -not_self_hosted_git(repo, message) :- repository(repo, name, remote, branch, release, commit_sha, commit_date), +not_self_hosted_git(repo, message) :- repository(repo, name, remote, branch, release, commit_sha, commit_date,_,_), match("^.*(github.com|gitlab.com).*$", remote), message=remote. /** @@ -40,7 +40,7 @@ transitive_dependency(repo, dependency) :- * Extract the id and full name from the repository relation. */ .decl is_repo(repo: number, repo_full_name: symbol) - is_repo(repo, name) :- repository(repo, name,_,_,_,_,_). + is_repo(repo, name) :- repository(repo, name,_,_,_,_,_,_,_). /** * ADT recursively describing a JSON object. diff --git a/src/macaron/slsa_analyzer/analyze_context.py b/src/macaron/slsa_analyzer/analyze_context.py index 0274217dc..eb02f26cc 100644 --- a/src/macaron/slsa_analyzer/analyze_context.py +++ b/src/macaron/slsa_analyzer/analyze_context.py @@ -56,6 +56,8 @@ def __init__( output_dir: str = "", remote_path: str = "", current_date: str = "", + namespace: str = "", + name: str = "", ): """Initialize instance. @@ -74,11 +76,17 @@ def __init__( commit_date : str The commit date of the target repo. macaron_path : str - The Macaron's root path. + The Macaron root path. output_dir : str The output dir. remote_path : str The remote path for the target repo. + current_date: str + The current date. + namespace : str + The purl namespace element, a.k.a group id. + name : str + The purl name element, a.k.a artifact id. """ # / self.repo_full_name = full_name @@ -123,6 +131,9 @@ def __init__( policy=None, ) + self.namespace = namespace + self.name = name + self.repository_table = RepositoryTable(**self.get_repository_data()) @property @@ -208,6 +219,8 @@ def get_repository_data(self) -> dict: "branch_name": self.branch_name, "commit_sha": self.commit_sha, "remote_path": self.remote_path, + "namespace": self.namespace, + "name": self.name, } def get_analysis_result_data(self) -> dict: diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index 97c1ec431..0104c2c0b 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -140,7 +140,7 @@ def run(self, user_config: dict, sbom_path: str = "", skip_deps: bool = False) - if skip_deps: logger.info("Skipping automatic dependency analysis...") else: - deps_resolved = self.resolve_dependencies(main_record.context, sbom_path) + deps_resolved = self.resolve_dependencies(self.db_man, main_record.context, sbom_path) # Merge the automatically resolved dependencies with the manual configuration. deps_config = DependencyAnalyzer.merge_configs(deps_config, deps_resolved) @@ -240,11 +240,15 @@ def generate_reports(self, report: Report) -> None: for reporter in self.reporters: reporter.generate(output_target_path, report) - def resolve_dependencies(self, main_ctx: AnalyzeContext, sbom_path: str) -> dict[str, DependencyInfo]: + def resolve_dependencies( + self, db_man: DatabaseManager, main_ctx: AnalyzeContext, sbom_path: str + ) -> dict[str, DependencyInfo]: """Resolve the dependencies of the main target repo. Parameters ---------- + db_man : DatabaseManager + The database manager for accessing the database. main_ctx : AnalyzeContext The context of object of the target repository. sbom_path: str @@ -257,7 +261,7 @@ def resolve_dependencies(self, main_ctx: AnalyzeContext, sbom_path: str) -> dict """ if sbom_path: logger.info("Getting the dependencies from the SBOM defined at %s.", sbom_path) - return get_deps_from_sbom(sbom_path) + return get_deps_from_sbom(db_man, sbom_path) deps_resolved: dict[str, DependencyInfo] = {} @@ -392,7 +396,7 @@ def run_single(self, config: Configuration, existing_records: Optional[dict[str, policies_passed=[], ) - analyze_ctx = self.get_analyze_ctx(req_branch, git_obj) + analyze_ctx = self.get_analyze_ctx(req_branch, git_obj, repo_id) analyze_ctx.dynamic_data["policy"] = self.policies.get_policy_for_target(analyze_ctx.repo_full_name) analyze_ctx.check_results = self.perform_checks(analyze_ctx) @@ -406,7 +410,7 @@ def run_single(self, config: Configuration, existing_records: Optional[dict[str, context=analyze_ctx, ) - def get_analyze_ctx(self, branch_name: str, git_obj: Git) -> AnalyzeContext: + def get_analyze_ctx(self, branch_name: str, git_obj: Git, repo_id: str) -> AnalyzeContext: """Return the analyze context for a target repository. Parameters @@ -417,6 +421,8 @@ def get_analyze_ctx(self, branch_name: str, git_obj: Git) -> AnalyzeContext: the current branch name cannot be determined. git_obj : Git The pydriller Git object of the target repository. + repo_id : str + The id of the target repository. Returns ------- @@ -470,6 +476,11 @@ def get_analyze_ctx(self, branch_name: str, git_obj: Git) -> AnalyzeContext: commit_date_str, ) + artifact_id = "" + group_id = "" + if defaults.getboolean("repofinder.java", "use_database") and not repo_id.startswith("http") and ":" in repo_id: + artifact_id, group_id = repo_id.split(":") + # Initialize the analyzing context for this repository. analyze_ctx = AnalyzeContext( full_name, @@ -481,6 +492,8 @@ def get_analyze_ctx(self, branch_name: str, git_obj: Git) -> AnalyzeContext: global_config.macaron_path, self.output_path, origin_remote_path, + group_id, + artifact_id, ) self.db_man.add(analyze_ctx.repository_table) diff --git a/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py b/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py index d7baf180e..6758a7828 100644 --- a/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py +++ b/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py @@ -74,7 +74,7 @@ def test_convert_components_to_artifacts(snapshot: dict[str, DependencyInfo]) -> # Pass a root bom.json and two sub-project bom.json files in recursive mode. result = convert_components_to_artifacts( - get_dep_components(root_bom_path=root_bom_path, child_bom_paths=child_bom_paths, recursive=True) + None, get_dep_components(root_bom_path=root_bom_path, child_bom_paths=child_bom_paths, recursive=True) ) assert snapshot == result @@ -93,7 +93,7 @@ def test_low_quality_bom(snapshot: dict[str, DependencyInfo], name: str) -> None """ # Path to the BOM file. bom_path = Path(RESOURCES_DIR, name) - result = get_deps_from_sbom(bom_path) + result = get_deps_from_sbom(None, bom_path) assert snapshot == result @@ -104,5 +104,5 @@ def test_multiple_versions(snapshot: dict[str, DependencyInfo]) -> None: """ # Path to the BOM file. bom_path = Path(RESOURCES_DIR, "bom_multi_versions.json") - result = get_deps_from_sbom(bom_path) + result = get_deps_from_sbom(None, bom_path) assert snapshot == result diff --git a/tests/dependency_analyzer/java_repo_finder/test_java_repo_finder.py b/tests/dependency_analyzer/java_repo_finder/test_java_repo_finder.py index 49eb3065e..a728dd73b 100644 --- a/tests/dependency_analyzer/java_repo_finder/test_java_repo_finder.py +++ b/tests/dependency_analyzer/java_repo_finder/test_java_repo_finder.py @@ -7,7 +7,13 @@ import os from pathlib import Path +import pytest +import sqlalchemy + from macaron.config.defaults import defaults +from macaron.config.global_config import global_config +from macaron.database.database_manager import DatabaseManager +from macaron.database.table_definitions import RepositoryTable from macaron.dependency_analyzer.java_repo_finder import create_urls, find_parent, find_scm, parse_pom @@ -48,3 +54,37 @@ def test_java_repo_finder_hierarchical() -> None: assert group == "owner" assert artifact == "parent" assert version == "1" + + +@pytest.fixture() +def database_fixture() -> DatabaseManager: # type: ignore + """Set up and tear down a test database.""" + database_path = os.path.join(global_config.output_path, "test.db") + db_man = DatabaseManager(database_path) + db_man.create_tables() + yield db_man + db_man.terminate() + os.remove(database_path) + + +def test_java_repo_database(database_fixture: DatabaseManager) -> None: # pylint: disable=redefined-outer-name + """Test the database functionality used by the repo finder.""" + data = { + "full_name": "test_repo", + "commit_date": "02/02/02", + "branch_name": "branch_1", + "commit_sha": "shashasha", + "remote_path": "remote_path", + "namespace": "namespace", + "name": "name", + } + table = RepositoryTable(**data) + database_fixture.add(table) + database_fixture.session.commit() + query = sqlalchemy.select(RepositoryTable).where( + RepositoryTable.namespace == "namespace", RepositoryTable.name == "name" + ) + result = database_fixture.execute_and_return(query) + row = result.first() + assert row is not None + assert row.remote_path == "remote_path" diff --git a/tests/policy_engine/resources/facts/macaron.db.gz b/tests/policy_engine/resources/facts/macaron.db.gz index 92f4b6cec..42426db3a 100644 Binary files a/tests/policy_engine/resources/facts/macaron.db.gz and b/tests/policy_engine/resources/facts/macaron.db.gz differ diff --git a/tests/policy_engine/resources/policies/testpolicy.dl b/tests/policy_engine/resources/policies/testpolicy.dl index d45e7b58e..e18207f96 100644 --- a/tests/policy_engine/resources/policies/testpolicy.dl +++ b/tests/policy_engine/resources/policies/testpolicy.dl @@ -7,30 +7,30 @@ Policy("trusted_builder", repo, name) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), check_passed(repo, "mcn_trusted_builder_level_three_1"). Policy("trusted_builder", repo, name) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), build_service_check(build_tool_name, ci_service, build_trigger, result_id, result, repo), passed = 1, match("github-actions", build_tool_name). Policy("trusted_builder", repo, name) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), build_script_check(build_tool_name, result_id, result, repo), passed = 1, match("github-actions", build_tool_name). apply_policy_to("trusted_builder", repo) :- - repository(repo, name,_,_,_,_,_), + repository(repo, name,_,_,_,_,_,_,_), provenance(prov, repo, _, _, _, _, _, _, _). apply_policy_to("aggregate_l4", repo) :- - repository(repo,"slsa-framework/slsa-verifier",_,_,_,_,_). + repository(repo,"slsa-framework/slsa-verifier",_,_,_,_,_,_,_). apply_policy_to("aggregate_l2", repo) :- - repository(repo,_ ,_,_,_,_,_). + repository(repo,_ ,_,_,_,_,_,_,_).