Skip to content

Commit a3842b4

Browse files
authored
feat: extend static analysis and compute confidence scores for deploy commands (#673)
This PR extends and makes changes to the static analysis of CI configurations in Macaron with the high-level goal of finding build and deploy commands more accurately. To achieve that, some of the abstractions had to be replaced to allow writing customized analyses, such as detecting build language setup in a GitHub Actions workflow or detecting reachable secrets. Here are the summary of changes: * BashCommands which consisted of build tool commands collected after analyzing GitHub Actions and passed to checks is replaced with BuildToolCommand. * The build related checks are refactored and simplified to use BuildToolCommand. * The callgraph analysis, which needs to be implemented for each CI service, is extended with new node types for GitHub Actions. The callgraph plays the role of Intermediate Representation and is available to all checks. * The mcn_build_script_1 check does not depend on any checks and always runs by default based on a customer request. * The mcn_build_as_code_1 check now reports deploy commands with confidence scores. * New analysis is added to resolve the value of expression variables, which is used for other analysis, such as reachable secrets and build language detection. * New abstractions are added to model third-party GitHub Actions. This feature is used to collect data about build Language setup Signed-off-by: behnazh-w <behnaz.hassanshahi@oracle.com>
1 parent f21bb6f commit a3842b4

File tree

70 files changed

+5144
-1890
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+5144
-1890
lines changed

docs/source/assets/er-diagram.svg

Lines changed: 215 additions & 260 deletions
Loading

docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.build_tool.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ macaron.slsa\_analyzer.build\_tool.gradle module
4141
:undoc-members:
4242
:show-inheritance:
4343

44+
macaron.slsa\_analyzer.build\_tool.language module
45+
--------------------------------------------------
46+
47+
.. automodule:: macaron.slsa_analyzer.build_tool.language
48+
:members:
49+
:undoc-members:
50+
:show-inheritance:
51+
4452
macaron.slsa\_analyzer.build\_tool.maven module
4553
-----------------------------------------------
4654

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
macaron.slsa\_analyzer.ci\_service.github\_actions package
2+
==========================================================
3+
4+
.. automodule:: macaron.slsa_analyzer.ci_service.github_actions
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:
8+
9+
Submodules
10+
----------
11+
12+
macaron.slsa\_analyzer.ci\_service.github\_actions.analyzer module
13+
------------------------------------------------------------------
14+
15+
.. automodule:: macaron.slsa_analyzer.ci_service.github_actions.analyzer
16+
:members:
17+
:undoc-members:
18+
:show-inheritance:
19+
20+
macaron.slsa\_analyzer.ci\_service.github\_actions.github\_actions\_ci module
21+
-----------------------------------------------------------------------------
22+
23+
.. automodule:: macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci
24+
:members:
25+
:undoc-members:
26+
:show-inheritance:

docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.ci_service.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ macaron.slsa\_analyzer.ci\_service package
66
:undoc-members:
77
:show-inheritance:
88

9+
Subpackages
10+
-----------
11+
12+
.. toctree::
13+
:maxdepth: 1
14+
15+
macaron.slsa_analyzer.ci_service.github_actions
16+
917
Submodules
1018
----------
1119

@@ -25,14 +33,6 @@ macaron.slsa\_analyzer.ci\_service.circleci module
2533
:undoc-members:
2634
:show-inheritance:
2735

28-
macaron.slsa\_analyzer.ci\_service.github\_actions module
29-
---------------------------------------------------------
30-
31-
.. automodule:: macaron.slsa_analyzer.ci_service.github_actions
32-
:members:
33-
:undoc-members:
34-
:show-inheritance:
35-
3636
macaron.slsa\_analyzer.ci\_service.gitlab\_ci module
3737
----------------------------------------------------
3838

golang/internal/bashparser/bashparser.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright (c) 2022 - 2022, Oracle and/or its affiliates. All rights reserved. */
1+
/* Copyright (c) 2022 - 2024, 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
// Package bashparser parses the bash scripts and provides parsed objects in JSON.
@@ -21,16 +21,17 @@ type CMDResult struct {
2121
// ParseCommands parses the bash script to find bash commands.
2222
// It returns the parsed commands in JSON format.
2323
func ParseCommands(data string) (string, error) {
24-
// Remove GitHub Actions's expressions because the bash parser doesn't recognize it.
25-
// We use greedy matching, so if we have `${{ $ {{ foo }} }}`, it will be matched
26-
// to `$MACARON_UNKNOWN`, even though it's not a valid GitHub expression.
24+
// Replace GitHub Actions's expressions with ``$MACARON_UNKNOWN``` variable because the bash parser
25+
// doesn't recognize such expressions. For example: ``${{ foo }}`` will be replaced by ``$MACARON_UNKNOWN``.
26+
// Note that we don't use greedy matching, so if we have `${{ ${{ foo }} }}`, it will not be replaced by
27+
// `$MACARON_UNKNOWN`.
2728
// See: https://docs.github.com/en/actions/learn-github-actions/expressions.
28-
var re, reg_error = regexp.Compile(`\$\{\{.*\}\}`)
29+
var re, reg_error = regexp.Compile(`\$\{\{.*?\}\}`)
2930
if reg_error != nil {
3031
return "", reg_error
3132
}
3233

33-
// We replace the GH Actions variables with "UNKNOWN" for now.
34+
// We replace the GH Actions variables with "$MACARON_UNKNOWN".
3435
data = string(re.ReplaceAll([]byte(data), []byte("$$MACARON_UNKNOWN")))
3536
data_str := strings.NewReader(data)
3637
data_parsed, parse_err := syntax.NewParser().Parse(data_str, "")

scripts/dev_scripts/integration_tests.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ echo "Run integration tests without configurations"
8181
echo -e "==================================================================================\n"
8282

8383
echo -e "\n----------------------------------------------------------------------------------"
84-
echo "micronaut-projects/micronaut-core: Analyzing the repo path and the branch name when automatic dependency resolution is skipped."
84+
echo "micronaut-projects/micronaut-core: Analyzing the PURL when automatic dependency resolution is skipped."
8585
echo -e "----------------------------------------------------------------------------------\n"
8686
JSON_EXPECTED=$WORKSPACE/tests/e2e/expected_results/purl/maven/micronaut-core/micronaut-core.json
8787
JSON_RESULT=$WORKSPACE/output/reports/maven/io_micronaut/micronaut-core/micronaut-core.json
@@ -705,6 +705,17 @@ $RUN_POLICY -f $POLICY_FILE -d "$WORKSPACE/output/macaron.db" || log_fail
705705
check_or_update_expected_output $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
706706
check_or_update_expected_output "$COMPARE_VSA" "$VSA_RESULT" "$VSA_PAYLOAD_EXPECTED" || log_fail
707707

708+
echo -e "\n----------------------------------------------------------------------------------"
709+
echo "Run policy CLI with micronaut-core results to test deploy command information."
710+
echo -e "----------------------------------------------------------------------------------\n"
711+
RUN_POLICY="macaron verify-policy"
712+
POLICY_FILE=$WORKSPACE/tests/policy_engine/resources/policies/micronaut-core/test_deploy_info.dl
713+
POLICY_RESULT=$WORKSPACE/output/policy_report.json
714+
POLICY_EXPECTED=$WORKSPACE/tests/policy_engine/expected_results/micronaut-core/test_deploy_info.json
715+
716+
$RUN_POLICY -f $POLICY_FILE -d "$WORKSPACE/output/macaron.db" || log_fail
717+
check_or_update_expected_output $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
718+
708719
# Testing the Repo Finder's remote calls.
709720
# This requires the 'packageurl' Python module
710721
echo -e "\n----------------------------------------------------------------------------------"

src/macaron/code_analyzer/call_graph.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2022 - 2024, 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
"""This module contains classes to generate build call graphs for the target repository."""
55

66
from collections import deque
77
from collections.abc import Iterable
8-
from typing import Generic, TypeVar
8+
from typing import Any, Generic, TypeVar
99

1010
Node = TypeVar("Node", bound="BaseNode")
1111
# The documentation below for `TypeVar` is commented out due to a breaking
@@ -21,9 +21,22 @@
2121
class BaseNode(Generic[Node]):
2222
"""This is the generic class for call graph nodes."""
2323

24-
def __init__(self) -> None:
25-
"""Initialize instance."""
24+
def __init__(self, caller: Node | None = None, node_id: str | None = None) -> None:
25+
"""Initialize instance.
26+
27+
Parameter
28+
---------
29+
caller: Node | None
30+
The caller node.
31+
node_id: str | None
32+
The unique identifier of a node in the callgraph.
33+
"""
2634
self.callee: list[Node] = []
35+
self.caller: Node | None = caller
36+
# Each node can have a model that summarizes certain properties for static analysis.
37+
# By default this model is set to None.
38+
self.model: Any = None
39+
self.node_id = node_id
2740

2841
def add_callee(self, node: Node) -> None:
2942
"""Add a callee to the current node.

src/macaron/config/defaults.ini

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ timeout = 30
3030
[bashparser]
3131
# This is the timeout (in seconds) for the bashparser.
3232
timeout = 30
33+
# The maximum allowed recursion depth when scripts call other scripts.
34+
recursion_depth = 3
3335

3436
[cue_validator]
3537
# This is the timeout (in seconds) for the cue_validator.
@@ -199,6 +201,7 @@ builder =
199201
gradle
200202
gradlew
201203
build_arg =
204+
build
202205
deploy_arg =
203206
artifactoryPublish
204207
publish
@@ -278,7 +281,6 @@ interpreter =
278281
interpreter_flag =
279282
-m
280283
build_arg =
281-
install
282284
build
283285
setup.py
284286
deploy_arg =
@@ -342,11 +344,14 @@ package_lock =
342344
builder =
343345
npm
344346
pnpm
345-
# Build args not defined since npm build is just a plumbing command https://docs.npmjs.com/cli/v6/commands/npm-build
346-
# and SLSA v1.0 removes the scripted build requirement https://slsa.dev/spec/v1.0/requirements
347347
build_arg =
348+
install
349+
build_run_arg =
350+
build
348351
deploy_arg =
349352
publish
353+
deploy_run_arg =
354+
publish
350355
[builder.npm.ci.deploy]
351356
github_actions =
352357
JS-DevTools/npm-publish
@@ -371,17 +376,23 @@ package_lock =
371376
yarn.lock
372377
builder =
373378
yarn
374-
# Build args not defined for similar reasons to npm
375379
build_arg =
380+
build
381+
build_run_arg =
382+
build
376383
deploy_arg =
377384
publish
385+
deploy_run_arg =
386+
publish
378387

379388
# This is the spec for trusted Go build tool usages.
380389
[builder.go]
381390
entry_conf =
382391
build_configs =
383392
go.mod
384393
go.sum
394+
.goreleaser.yaml
395+
.goreleaser.yml
385396
builder =
386397
go
387398
build_arg =
@@ -411,6 +422,17 @@ trusted_builders =
411422
slsa-framework/slsa-github-generator/.github/workflows/builder_container-based_slsa3.yml
412423
# The number of days that GitHub Actions persists the workflow run.
413424
max_workflow_persist = 90
425+
# The GitHub Actions configuration workflow files for third-party tools.
426+
third_party_configurations =
427+
codeql-analysis.yaml
428+
codeql-analysis.yml
429+
codeql-config.yaml
430+
codeql-config.yml
431+
scorecards-analysis.yaml
432+
scorecards-analysis.yml
433+
dependabot.yaml
434+
dependabot.yml
435+
renovate.json
414436

415437
# This is the spec for Jenkins CI.
416438
[ci.jenkins]

src/macaron/errors.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,15 @@ class JsonError(MacaronError):
7070

7171
class InvalidAnalysisTargetError(MacaronError):
7272
"""When a valid Analysis Target cannot be constructed."""
73+
74+
75+
class ParseError(MacaronError):
76+
"""The errors related to parsers."""
77+
78+
79+
class CallGraphError(MacaronError):
80+
"""The errors related to callgraphs."""
81+
82+
83+
class GitHubActionsValueError(MacaronError):
84+
"""The errors related to GitHub Actions value errors."""

src/macaron/parsers/actionparser.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2022 - 2022, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2022 - 2024, 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
"""This module is a Python wrapper for the compiled actionparser binary.
@@ -13,9 +13,12 @@
1313
import logging
1414
import os
1515
import subprocess # nosec B404
16+
from typing import Any
1617

1718
from macaron.config.defaults import defaults
1819
from macaron.config.global_config import global_config
20+
from macaron.errors import JsonError, ParseError
21+
from macaron.json_tools import json_extract
1922

2023
logger: logging.Logger = logging.getLogger(__name__)
2124

@@ -34,6 +37,11 @@ def parse(workflow_path: str, macaron_path: str = "") -> dict:
3437
-------
3538
dict
3639
The parsed workflow as a JSON (dict) object.
40+
41+
Raises
42+
------
43+
ParseError
44+
When parsing fails with errors.
3745
"""
3846
if not macaron_path:
3947
macaron_path = global_config.macaron_path
@@ -56,15 +64,59 @@ def parse(workflow_path: str, macaron_path: str = "") -> dict:
5664
subprocess.TimeoutExpired,
5765
FileNotFoundError,
5866
) as error:
59-
logger.error("Error while parsing GitHub Action workflow %s: %s", workflow_path, error)
60-
return {}
67+
raise ParseError(f"Error while parsing GitHub Action workflow {workflow_path}") from error
6168

6269
try:
6370
if result.returncode == 0:
6471
parsed_obj: dict = json.loads(result.stdout.decode("utf-8"))
6572
return parsed_obj
66-
logger.error("GitHub Actions parser failed: %s", result.stderr)
67-
return {}
73+
raise ParseError(f"GitHub Actions parser failed: {result.stderr.decode('utf-8')}")
6874
except json.JSONDecodeError as error:
69-
logger.error("Error while loading the parsed Actions workflow: %s", error)
70-
return {}
75+
raise ParseError("Error while loading the parsed Actions workflow") from error
76+
77+
78+
def get_run_step(step: dict[str, Any]) -> str | None:
79+
"""Get the parsed GitHub Action run step for inlined shell scripts.
80+
81+
If the run step cannot be validated this function returns None.
82+
83+
Parameters
84+
----------
85+
step: dict[str, Any]
86+
The parsed step object.
87+
88+
Returns
89+
-------
90+
str | None
91+
The inlined run script or None if the run step cannot be validated.
92+
"""
93+
try:
94+
return json_extract(step, ["Exec", "Run", "Value"], str)
95+
except JsonError as error:
96+
logger.debug(error)
97+
return None
98+
99+
100+
def get_step_input(step: dict[str, Any], key: str) -> str | None:
101+
"""Get an input value from a GitHub Action step.
102+
103+
If the input value cannot be found or the step inputs cannot be validated this function
104+
returns None.
105+
106+
Parameters
107+
----------
108+
step: dict[str, Any]
109+
The parsed step object.
110+
key: str
111+
The key to be looked up.
112+
113+
Returns
114+
-------
115+
str | None
116+
The input value or None if it doesn't exist or the parsed object validation fails.
117+
"""
118+
try:
119+
return json_extract(step, ["Exec", "Inputs", key, "Value", "Value"], str)
120+
except JsonError as error:
121+
logger.debug(error)
122+
return None

0 commit comments

Comments
 (0)