Skip to content

Commit a684508

Browse files
committed
chore: address PR feedback
Signed-off-by: behnazh-w <[email protected]>
1 parent e13d576 commit a684508

File tree

8 files changed

+87
-69
lines changed

8 files changed

+87
-69
lines changed

docs/source/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Current checks in Macaron
4646
The table below shows the current set of actionable checks derived from
4747
the requirements that are currently supported by Macaron.
4848

49-
.. list-table:: Macaron checks descriptions
49+
.. list-table:: Macaron check descriptions
5050
:widths: 20 40 40
5151
:header-rows: 1
5252

docs/source/pages/cli_usage/command_analyze.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved.
1+
.. Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved.
22
.. Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
33
44
.. _analyze-command-cli:

docs/source/pages/tutorials/detect_vulnerable_github_actions.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
How to detect vulnerable GitHub Actions
88
=======================================
99

10-
This tutorial explains how to use a check in Macaron that detects vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions like `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_.
10+
This tutorial explains how to use a check in Macaron to detect vulnerable third-party GitHub Actions. This check is important for preventing security issues in your CI/CD pipeline, especially in light of recent incidents, such as vulnerabilities discovered in popular GitHub Actions like `tj-actions/changed-files <https://www.cve.org/CVERecord?id=CVE-2025-30066>`_, and `reviewdog/action-setup <https://www.cve.org/CVERecord?id=CVE-2025-30154>`_.
1111

12-
We will guide you on how to enable and use this check to enhance the security of your development pipeline.
12+
We will demonstrate how to enable and use this check to enhance the security of your development pipeline.
1313

1414
For more information on other features of Macaron, please refer to the :ref:`documentation here <index>`.
1515

src/macaron/slsa_analyzer/checks/github_actions_vulnerability_check.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,16 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
8383
CheckResultData
8484
The result of the check.
8585
"""
86-
result_tables: list[CheckFacts] = []
87-
8886
ci_services = ctx.dynamic_data["ci_services"]
8987

9088
external_workflows: dict[str, list] = {}
9189
for ci_info in ci_services:
9290
for callee in ci_info["callgraph"].bfs():
93-
if isinstance(callee, GitHubWorkflowNode) and callee.node_type in [
91+
if isinstance(callee, GitHubWorkflowNode) and callee.node_type in {
9492
GitHubWorkflowType.EXTERNAL,
9593
GitHubWorkflowType.REUSABLE,
96-
]:
94+
}:
95+
workflow_name = workflow_version = ""
9796
if "@" in callee.name:
9897
workflow_name, workflow_version = callee.name.split("@")
9998
else:
@@ -104,7 +103,10 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
104103

105104
caller_path = callee.caller.source_path if callee.caller else None
106105

107-
if not workflow_name:
106+
# Skip the workflow if `workflow_name` or `workflow_version` are missing,
107+
# or if `callee.name` lacks an '@' which can indicate an internal workflow
108+
# within the same repo .
109+
if not workflow_name or not workflow_version:
108110
logger.debug("Workflow %s is not relevant. Skipping...", callee.name)
109111
continue
110112

@@ -144,6 +146,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
144146
except APIAccessError as error:
145147
logger.debug(error)
146148

149+
result_tables: list[CheckFacts] = []
147150
for vuln_res in batch_vulns:
148151
vulns: list = []
149152
workflow_name = vuln_res["name"]

src/macaron/slsa_analyzer/git_url.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,7 @@ def is_empty_repo(git_obj: Git) -> bool:
909909
return True
910910

911911

912-
def is_commit_hash(version_str: str) -> bool:
912+
def is_commit_hash(value: str) -> bool:
913913
"""Check if a given string is a valid Git commit hash.
914914
915915
A valid Git commit hash is a 40-character long hexadecimal string or
@@ -918,7 +918,8 @@ def is_commit_hash(version_str: str) -> bool:
918918
919919
Parameters
920920
----------
921-
version_str (str): The string to be checked for validity as a commit hash.
921+
value: str
922+
The string value to be checked for validity as a commit hash.
922923
923924
Returns
924925
-------
@@ -939,7 +940,7 @@ def is_commit_hash(version_str: str) -> bool:
939940
False
940941
"""
941942
pattern = r"^[a-f0-9]{7,40}$"
942-
return bool(re.match(pattern, version_str))
943+
return bool(re.match(pattern, value))
943944

944945

945946
def get_tags_via_git_remote(repo: str) -> dict[str, str] | None:

src/macaron/slsa_analyzer/package_registry/osv_dev.py

+59-48
Original file line numberDiff line numberDiff line change
@@ -118,32 +118,34 @@ def get_vulnerabilities_package_name_batch(packages: list) -> list:
118118
return results
119119

120120
@staticmethod
121-
def call_osv_query_api(query_data: dict) -> list:
122-
"""Query the OSV (Open Source Vulnerability) knowledge base API with the given data.
121+
def get_osv_url(endpoint: str) -> str:
122+
"""Construct a full API URL for a given OSV endpoint using values from the .ini configuration.
123123
124-
This method sends a POST request to the OSV API and processes the response to extract
125-
information about vulnerabilities based on the provided query data.
124+
The configuration is expected to be in a section named `[osv_dev]` within the defaults object,
125+
and must include the following keys:
126+
127+
- `url_netloc`: The base domain of the API.
128+
- `url_scheme` (optional): The scheme (e.g., "https"). Defaults to "https" if not provided.
129+
- A key matching the provided `endpoint` argument (e.g., "query_endpoint"), which defines the URL path.
126130
127131
Parameters
128132
----------
129-
query_data : dict
130-
A dictionary containing the query parameters to be sent to the OSV API.
131-
The query data should conform to the format expected by the OSV API for querying vulnerabilities.
133+
endpoint: str
134+
The key name of the endpoint in the `[osv_dev]` section to construct the URL path.
132135
133136
Returns
134137
-------
135-
list
136-
A list of vulnerabilities under the key "vulns" if the query is successful
137-
and the response is valid.
138+
str
139+
The fully constructed API URL.
138140
139141
Raises
140142
------
141143
APIAccessError
142-
If there are issues with the API URL construction, missing configuration values, or invalid responses.
144+
If required keys are missing from the configuration or if the URL cannot be constructed.
143145
"""
144146
section_name = "osv_dev"
145147
if not defaults.has_section(section_name):
146-
return []
148+
raise APIAccessError(f"The section [{section_name}] is missing in the .ini configuration file.")
147149
section = defaults[section_name]
148150

149151
url_netloc = section.get("url_netloc")
@@ -152,13 +154,13 @@ def call_osv_query_api(query_data: dict) -> list:
152154
f'The "url_netloc" key is missing in section [{section_name}] of the .ini configuration file.'
153155
)
154156
url_scheme = section.get("url_scheme", "https")
155-
query_endpoint = section.get("query_endpoint")
157+
query_endpoint = section.get(endpoint)
156158
if not query_endpoint:
157159
raise APIAccessError(
158160
f'The "query_endpoint" key is missing in section [{section_name}] of the .ini configuration file.'
159161
)
160162
try:
161-
url = urllib.parse.urlunsplit(
163+
return urllib.parse.urlunsplit(
162164
urllib.parse.SplitResult(
163165
scheme=url_scheme,
164166
netloc=url_netloc,
@@ -170,6 +172,34 @@ def call_osv_query_api(query_data: dict) -> list:
170172
except ValueError as error:
171173
raise APIAccessError("Failed to construct the API URL.") from error
172174

175+
@staticmethod
176+
def call_osv_query_api(query_data: dict) -> list:
177+
"""Query the OSV (Open Source Vulnerability) knowledge base API with the given data.
178+
179+
This method sends a POST request to the OSV API and processes the response to extract
180+
information about vulnerabilities based on the provided query data.
181+
182+
Parameters
183+
----------
184+
query_data : dict
185+
A dictionary containing the query parameters to be sent to the OSV API.
186+
The query data should conform to the format expected by the OSV API for querying vulnerabilities.
187+
188+
Returns
189+
-------
190+
list
191+
A list of vulnerabilities under the key "vulns" if the query is successful
192+
and the response is valid.
193+
194+
Raises
195+
------
196+
APIAccessError
197+
If there are issues with the API URL construction, missing configuration values, or invalid responses.
198+
"""
199+
try:
200+
url = OSVDevService.get_osv_url("query_endpoint")
201+
except APIAccessError as error:
202+
raise error
173203
response = send_post_http_raw(url, json_data=query_data, headers=None)
174204
res_obj = None
175205
if response:
@@ -209,8 +239,7 @@ def call_osv_querybatch_api(query_data: dict, expected_size: int | None = None)
209239
-------
210240
list
211241
A list of results from the OSV API containing the vulnerability data that matches
212-
the query parameters. If no valid response is received or the results are
213-
improperly formatted, an empty list is returned.
242+
the query parameters.
214243
215244
Raises
216245
------
@@ -219,34 +248,10 @@ def call_osv_querybatch_api(query_data: dict, expected_size: int | None = None)
219248
fails, or if the response from the OSV API is invalid or the number of results
220249
does not match the expected size.
221250
"""
222-
section_name = "osv_dev"
223-
if not defaults.has_section(section_name):
224-
return []
225-
section = defaults[section_name]
226-
227-
url_netloc = section.get("url_netloc")
228-
if not url_netloc:
229-
raise APIAccessError(
230-
f'The "url_netloc" key is missing in section [{section_name}] of the .ini configuration file.'
231-
)
232-
url_scheme = section.get("url_scheme", "https")
233-
query_endpoint = section.get("querybatch_endpoint")
234-
if not query_endpoint:
235-
raise APIAccessError(
236-
f'The "query_endpoint" key is missing in section [{section_name}] of the .ini configuration file.'
237-
)
238251
try:
239-
url = urllib.parse.urlunsplit(
240-
urllib.parse.SplitResult(
241-
scheme=url_scheme,
242-
netloc=url_netloc,
243-
path=query_endpoint,
244-
query="",
245-
fragment="",
246-
)
247-
)
248-
except ValueError as error:
249-
raise APIAccessError("Failed to construct the API URL.") from error
252+
url = OSVDevService.get_osv_url("querybatch_endpoint")
253+
except APIAccessError as error:
254+
raise error
250255

251256
response = send_post_http_raw(url, json_data=query_data, headers=None)
252257
res_obj = None
@@ -261,11 +266,13 @@ def call_osv_querybatch_api(query_data: dict, expected_size: int | None = None)
261266
if isinstance(results, list):
262267
if expected_size:
263268
if len(results) != expected_size:
264-
raise APIAccessError(f"Unable to get a valid result from {url}")
269+
raise APIAccessError(
270+
f"Failed to retrieve a valid result from {url}: result count does not match the expected count."
271+
)
265272

266273
return results
267274

268-
return []
275+
raise APIAccessError(f"The response from {url} does not contain a valid 'results' list.")
269276

270277
@staticmethod
271278
def is_version_affected(
@@ -326,9 +333,13 @@ def is_version_affected(
326333
pkg_version = tag
327334
break
328335

336+
# If we were not able to find a tag for the commit hash, raise an exception.
337+
if is_commit_hash(pkg_version):
338+
raise APIAccessError(f"Failed to find a tag for {pkg_name}@{pkg_version}.")
339+
329340
affected = json_extract(vuln, ["affected"], list)
330341
if not affected:
331-
raise APIAccessError(f"Failed to extracted info for {pkg_name}@{pkg_version}.")
342+
raise APIAccessError(f"Received invalid response for {pkg_name}@{pkg_version}.")
332343

333344
affected_ranges: list | None = None
334345
for rec in affected:
@@ -342,12 +353,12 @@ def is_version_affected(
342353
break
343354

344355
if not affected_ranges:
345-
raise APIAccessError(f"Failed to extracted affected versions for {pkg_name}@{pkg_version}.")
356+
raise APIAccessError(f"Failed to extract affected versions for {pkg_name}@{pkg_version}.")
346357

347358
for affected_range in affected_ranges:
348359
events = json_extract(affected_range, ["events"], list)
349360
if not events:
350-
raise APIAccessError(f"Failed to extracted affected versions for {pkg_name}@{pkg_version}.")
361+
raise APIAccessError(f"Failed to extract affected versions for {pkg_name}@{pkg_version}.")
351362

352363
introduced = None
353364
fixed = None

tests/slsa_analyzer/checks/test_github_actions_vulnerability_check.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,11 @@ def get_ci_info(ci_services: dict[str, BaseCIService], ci_name: str, workflow_pa
3535
provenances=[],
3636
build_info_results=InTotoV01Payload(statement=InferredProvenance().payload),
3737
)
38-
match ci_name:
39-
case "github_actions":
40-
root_node: BaseNode = BaseNode()
41-
workflow_node = build_call_graph_from_path(root_node, workflow_path=workflow_path, repo_path="")
42-
root_node.add_callee(workflow_node)
43-
ci_info["callgraph"] = CallGraph(root_node, "")
38+
if ci_name == "github_actions":
39+
root_node: BaseNode = BaseNode()
40+
workflow_node = build_call_graph_from_path(root_node, workflow_path=workflow_path, repo_path="")
41+
root_node.add_callee(workflow_node)
42+
ci_info["callgraph"] = CallGraph(root_node, "")
4443

4544
return ci_info
4645

tests/slsa_analyzer/package_registry/test_osv_dev.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,19 @@ def test_load_defaults_query_api(tmp_path: Path, user_config_input: str) -> None
5454

5555
def test_is_affected_version_invalid_commit() -> None:
5656
"""Test if the function can handle invalid commits"""
57-
with pytest.raises(APIAccessError):
57+
with pytest.raises(APIAccessError, match="^Failed to find a tag for"):
5858
OSVDevService.is_version_affected(
59-
vuln={}, pkg_name="pkg", pkg_version="invalid_commit", ecosystem="GitHub Actions"
59+
vuln={},
60+
pkg_name="pkg",
61+
pkg_version="c253e1f19ebfb98fe02a8354082cbbd282d446a0",
62+
ecosystem="GitHub Actions",
63+
source_repo="mock_repo",
6064
)
6165

6266

6367
def test_is_affected_version_invalid_response() -> None:
6468
"""Test if the function can handle empty OSV response."""
65-
with pytest.raises(APIAccessError):
69+
with pytest.raises(APIAccessError, match="^Received invalid response for"):
6670
OSVDevService.is_version_affected(
6771
vuln={"vulns": []}, pkg_name="repo/workflow", pkg_version="1.0.0", ecosystem="GitHub Actions"
6872
)

0 commit comments

Comments
 (0)