1
- # Copyright (c) 2022 - 2022 , Oracle and/or its affiliates. All rights reserved.
1
+ # Copyright (c) 2022 - 2023 , Oracle and/or its affiliates. All rights reserved.
2
2
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3
3
4
4
"""This module contains the BuildAsCodeCheck class."""
5
5
6
6
import logging
7
7
import os
8
8
9
+ from macaron .config .defaults import defaults
9
10
from macaron .slsa_analyzer .analyze_context import AnalyzeContext
10
11
from macaron .slsa_analyzer .build_tool .base_build_tool import BaseBuildTool , NoneBuildTool
11
12
from macaron .slsa_analyzer .checks .base_check import BaseCheck
12
13
from macaron .slsa_analyzer .checks .check_result import CheckResult , CheckResultType
13
14
from macaron .slsa_analyzer .ci_service .base_ci_service import NoneCIService
14
15
from macaron .slsa_analyzer .ci_service .circleci import CircleCI
16
+ from macaron .slsa_analyzer .ci_service .github_actions import GHWorkflowType
15
17
from macaron .slsa_analyzer .ci_service .gitlab_ci import GitLabCI
16
18
from macaron .slsa_analyzer .ci_service .jenkins import Jenkins
17
19
from macaron .slsa_analyzer .ci_service .travis import Travis
@@ -50,23 +52,43 @@ def __init__(self) -> None:
50
52
def _has_deploy_command (self , commands : list [list [str ]], build_tool : BaseBuildTool ) -> str :
51
53
"""Check if the bash command is a build and deploy command."""
52
54
for com in commands :
55
+
53
56
# Check for empty or invalid commands.
54
57
if not com or not com [0 ]:
55
58
continue
56
59
# The first argument in a bash command is the program name.
57
60
# So first check that the program name is a supported build tool name.
58
61
# We need to handle cases where the the first argument is a path to the program.
62
+ # TODO: support for not being a supported build tool first
59
63
cmd_program_name = os .path .basename (com [0 ])
60
64
if not cmd_program_name :
61
65
logger .debug ("Found invalid program name %s." , com [0 ])
62
66
continue
63
- if any (build_cmd for build_cmd in build_tool .builder if build_cmd == cmd_program_name ):
64
- # Check the arguments in the bash command for the deploy goals.
65
- # If there are no deploy args for this build tool, accept as deploy command.
67
+
68
+ # Account for Python projects having separate tools for packaging and publishing.
69
+ if build_tool .publisher :
70
+ deploy_tool = build_tool .publisher
71
+ else :
72
+ deploy_tool = build_tool .builder
73
+
74
+ check_build_commands = any (build_cmd for build_cmd in deploy_tool if build_cmd == cmd_program_name )
75
+
76
+ # Support the use of python modules, i.e. 'python -m pip install'
77
+ check_module_build_commands = any (
78
+ module == cmd_program_name and com [1 ] and com [1 ] == "-m" and com [2 ] in deploy_tool
79
+ for module in build_tool .builder_module
80
+ )
81
+ prog_name_index = 2 if check_module_build_commands else 0
82
+
83
+ # logger.info("com: %s", com[(prog_name_index + 1) :])
84
+
85
+ if check_build_commands or check_module_build_commands :
66
86
if not build_tool .deploy_arg :
67
87
logger .info ("No deploy arguments required. Accept %s as deploy command." , str (com ))
68
88
return str (com )
69
- for word in com [1 :]:
89
+ logger .info (build_tool .deploy_arg )
90
+
91
+ for word in com [(prog_name_index + 1 ) :]:
70
92
# TODO: allow plugin versions in arguments, e.g., maven-plugin:1.6.8:deploy.
71
93
if word in build_tool .deploy_arg :
72
94
logger .info ("Found deploy command %s." , str (com ))
@@ -90,7 +112,7 @@ def run_check(self, ctx: AnalyzeContext, check_result: CheckResult) -> CheckResu
90
112
"""
91
113
# Get the build tool identified by the mcn_version_control_system_1, which we depend on.
92
114
build_tool = ctx .dynamic_data ["build_spec" ].get ("tool" )
93
- ci_services = ctx .dynamic_data ["ci_services" ]
115
+ ci_services = ctx .dynamic_data ["ci_services" ] # keep storing things here
94
116
95
117
# Checking if a build tool is discovered for this repo.
96
118
if build_tool and not isinstance (build_tool , NoneBuildTool ):
@@ -99,6 +121,67 @@ def run_check(self, ctx: AnalyzeContext, check_result: CheckResult) -> CheckResu
99
121
# Checking if a CI service is discovered for this repo.
100
122
if isinstance (ci_service , NoneCIService ):
101
123
continue
124
+
125
+ trusted_deploy_actions = defaults .get_list ("builder.pip.ci.deploy" , "github_actions" , fallback = [])
126
+
127
+ # Check for use of a trusted Github Action to publish/deploy.
128
+ # TODO: verify that deplyment is legitimate and not a test
129
+ if trusted_deploy_actions :
130
+ for callee in ci_info ["callgraph" ].bfs ():
131
+ workflow_name = callee .name .split ("@" )[0 ]
132
+
133
+ if not workflow_name or callee .node_type not in [
134
+ GHWorkflowType .EXTERNAL ,
135
+ GHWorkflowType .REUSABLE ,
136
+ ]:
137
+ logger .debug ("Workflow %s is not relevant. Skipping..." , callee .name )
138
+ continue
139
+ if workflow_name in trusted_deploy_actions :
140
+ trigger_link = ci_service .api_client .get_file_link (
141
+ ctx .repo_full_name ,
142
+ ctx .commit_sha ,
143
+ ci_service .api_client .get_relative_path_of_workflow (
144
+ os .path .basename (callee .caller_path )
145
+ ),
146
+ )
147
+ # TODO: verify that caller_path and CI_PATH are the same
148
+ deploy_action_source_link = ci_service .api_client .get_file_link (
149
+ ctx .repo_full_name , ctx .commit_sha , callee .caller_path
150
+ )
151
+
152
+ html_url = ci_service .has_latest_run_passed (
153
+ ctx .repo_full_name ,
154
+ ctx .branch_name ,
155
+ ctx .commit_sha ,
156
+ ctx .commit_date ,
157
+ os .path .basename (callee .caller_path ),
158
+ )
159
+
160
+ # TODO: include in the justification multiple cases of external action usage
161
+ justification : list [str | dict [str , str ]] = [
162
+ {
163
+ f"The target repository uses build tool { build_tool .name } "
164
+ " to deploy" : deploy_action_source_link ,
165
+ "The build is triggered by" : trigger_link ,
166
+ },
167
+ f"Deploy action: { workflow_name } " ,
168
+ {"The status of the build can be seen at" : html_url }
169
+ if html_url
170
+ else "However, could not find a passing workflow run." ,
171
+ ]
172
+ check_result ["justification" ].extend (justification )
173
+ if ctx .dynamic_data ["is_inferred_prov" ] and ci_info ["provenances" ]:
174
+ predicate = ci_info ["provenances" ][0 ]["predicate" ]
175
+ predicate ["buildType" ] = f"Custom { ci_service .name } "
176
+ predicate ["builder" ]["id" ] = deploy_action_source_link
177
+ predicate ["invocation" ]["configSource" ][
178
+ "uri"
179
+ ] = f"{ ctx .remote_path } @refs/heads/{ ctx .branch_name } "
180
+ predicate ["invocation" ]["configSource" ]["digest" ]["sha1" ] = ctx .commit_sha
181
+ predicate ["invocation" ]["configSource" ]["entryPoint" ] = trigger_link
182
+ predicate ["metadata" ]["buildInvocationId" ] = html_url
183
+ return CheckResultType .PASSED
184
+
102
185
for bash_cmd in ci_info ["bash_commands" ]:
103
186
deploy_cmd = self ._has_deploy_command (bash_cmd ["commands" ], build_tool )
104
187
if deploy_cmd :
@@ -121,7 +204,7 @@ def run_check(self, ctx: AnalyzeContext, check_result: CheckResult) -> CheckResu
121
204
os .path .basename (bash_cmd ["CI_path" ]),
122
205
)
123
206
124
- justification : list [str | dict [str , str ]] = [
207
+ justification_cmd : list [str | dict [str , str ]] = [
125
208
{
126
209
f"The target repository uses build tool { build_tool .name } to deploy" : bash_source_link ,
127
210
"The build is triggered by" : trigger_link ,
@@ -131,7 +214,7 @@ def run_check(self, ctx: AnalyzeContext, check_result: CheckResult) -> CheckResu
131
214
if html_url
132
215
else "However, could not find a passing workflow run." ,
133
216
]
134
- check_result ["justification" ].extend (justification )
217
+ check_result ["justification" ].extend (justification_cmd )
135
218
if ctx .dynamic_data ["is_inferred_prov" ] and ci_info ["provenances" ]:
136
219
predicate = ci_info ["provenances" ][0 ]["predicate" ]
137
220
predicate ["buildType" ] = f"Custom { ci_service .name } "
0 commit comments