Skip to content

Commit b96eb05

Browse files
Merge pull request #1000 from GitGuardian/jgriffe/SCRT-3897/improve-scan-ci-commit-selection
feat(ci): improve scan ci commit selection
2 parents 740a0b1 + 1af4cfc commit b96eb05

File tree

4 files changed

+245
-256
lines changed

4 files changed

+245
-256
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Changed
2+
3+
- `ggshield secret scan ci` will now scan all of a Pull Request's commits in the following CI environments: Jenkins, Azure, Bitbucket and Drone.
4+
5+
### Fixed
6+
7+
- When running `ggshield secret scan ci` in a GitLab CI, new commits from the target branch that are not on the feature branch will no longer be scanned.

ggshield/core/git_hooks/ci/commit_range.py

Lines changed: 74 additions & 236 deletions
Original file line numberDiff line numberDiff line change
@@ -2,254 +2,92 @@
22
from typing import List, Tuple
33

44
from ggshield.core.errors import UnexpectedError
5+
from ggshield.core.git_hooks.ci.get_scan_ci_parameters import (
6+
CI_TARGET_BRANCH_ASSOC,
7+
get_remote_prefix,
8+
)
59
from ggshield.utils.git_shell import EMPTY_SHA, get_list_commit_SHA
610

711
from ... import ui
8-
from .previous_commit import (
9-
github_pull_request_previous_commit_sha,
10-
github_push_previous_commit_sha,
11-
gitlab_push_previous_commit_sha,
12-
)
1312
from .supported_ci import SupportedCI
1413

1514

16-
def collect_commit_range_from_ci_env() -> Tuple[List[str], SupportedCI]:
17-
supported_ci = SupportedCI.from_ci_env()
18-
try:
19-
fcn = COLLECT_COMMIT_RANGE_FUNCTIONS[supported_ci]
20-
except KeyError:
21-
raise UnexpectedError(f"Not implemented for {supported_ci.value}")
22-
23-
return fcn(), supported_ci
24-
25-
26-
def jenkins_range() -> List[str]: # pragma: no cover
27-
head_commit = os.getenv("GIT_COMMIT")
28-
previous_commit = os.getenv("GIT_PREVIOUS_COMMIT")
29-
30-
ui.display_verbose(
31-
f"\tGIT_COMMIT: {head_commit}" f"\nGIT_PREVIOUS_COMMIT: {previous_commit}"
32-
)
33-
34-
if previous_commit:
35-
commit_list = get_list_commit_SHA(f"{previous_commit}...{head_commit}")
36-
if commit_list:
37-
return commit_list
38-
39-
commit_list = get_list_commit_SHA(f"{head_commit}~1...")
40-
if commit_list:
41-
return commit_list
42-
43-
raise UnexpectedError(
44-
"Unable to get commit range. Please submit an issue with the following info:\n"
45-
"\tRepository URL: <Fill if public>\n"
46-
f"\tGIT_COMMIT: {head_commit}"
47-
f"\tGIT_PREVIOUS_COMMIT: {previous_commit}"
48-
)
49-
50-
51-
def travis_range() -> List[str]: # pragma: no cover
52-
commit_range = os.getenv("TRAVIS_COMMIT_RANGE")
53-
commit_sha = os.getenv("TRAVIS_COMMIT", "HEAD")
54-
55-
ui.display_verbose(
56-
f"TRAVIS_COMMIT_RANGE: {commit_range}" f"\nTRAVIS_COMMIT: {commit_sha}"
57-
)
58-
59-
if commit_range:
60-
commit_list = get_list_commit_SHA(commit_range)
61-
if commit_list:
62-
return commit_list
63-
64-
commit_list = get_list_commit_SHA(f"{commit_sha}~1...")
65-
if commit_list:
66-
return commit_list
67-
68-
raise UnexpectedError(
69-
"Unable to get commit range. Please submit an issue with the following info:\n"
70-
"\tRepository URL: <Fill if public>\n"
71-
f"\tTRAVIS_COMMIT_RANGE: {commit_range}"
72-
f"\tTRAVIS_COMMIT: {commit_sha}"
73-
)
74-
75-
76-
def bitbucket_pipelines_range() -> List[str]: # pragma: no cover
77-
commit_sha = os.getenv("BITBUCKET_COMMIT", "HEAD")
78-
ui.display_verbose(f"BITBUCKET_COMMIT: {commit_sha}")
79-
80-
commit_list = get_list_commit_SHA(f"{commit_sha}~1...")
81-
if commit_list:
82-
return commit_list
83-
84-
raise UnexpectedError(
85-
"Unable to get commit range. Please submit an issue with the following info:\n"
86-
" Repository URL: <Fill if public>\n"
87-
f" CI_COMMIT_SHA: {commit_sha}"
88-
)
89-
90-
91-
def circle_ci_range() -> List[str]: # pragma: no cover
92-
"""
93-
# Extract commit range (or single commit)
94-
COMMIT_RANGE=$(echo "${CIRCLE_COMPARE_URL}" | cut -d/ -f7)
95-
96-
# Fix single commit, unfortunately we don't always get a commit range from Circle CI
97-
if [[ $COMMIT_RANGE != *"..."* ]]; then
98-
COMMIT_RANGE="${COMMIT_RANGE}...${COMMIT_RANGE}"
99-
fi
100-
"""
101-
compare_range = os.getenv("CIRCLE_RANGE")
102-
commit_sha = os.getenv("CIRCLE_SHA1", "HEAD")
103-
104-
ui.display_verbose(f"CIRCLE_RANGE: {compare_range}\nCIRCLE_SHA1: {commit_sha}")
105-
106-
if compare_range and not compare_range.startswith("..."):
107-
commit_list = get_list_commit_SHA(compare_range)
108-
if commit_list:
109-
return commit_list
110-
111-
commit_list = get_list_commit_SHA(f"{commit_sha}~1...")
112-
if commit_list:
113-
return commit_list
114-
115-
raise UnexpectedError(
116-
"Unable to get commit range. Please submit an issue with the following info:\n"
117-
"\tRepository URL: <Fill if public>\n"
118-
f"\tCIRCLE_RANGE: {compare_range}\n"
119-
f"\tCIRCLE_SHA1: {commit_sha}"
120-
)
121-
122-
123-
def gitlab_ci_range() -> List[str]: # pragma: no cover
124-
before_sha = gitlab_push_previous_commit_sha()
125-
commit_sha = os.getenv("CI_COMMIT_SHA", "HEAD")
126-
merge_request_target_branch = os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")
127-
128-
ui.display_verbose(
129-
f"CI_MERGE_REQUEST_TARGET_BRANCH_NAME: {merge_request_target_branch}\n"
130-
f"CI_COMMIT_BEFORE_SHA: {before_sha}\n"
131-
f"CI_COMMIT_SHA: {commit_sha}"
132-
)
15+
CI_PREVIOUS_COMMIT_VAR = {
16+
SupportedCI.JENKINS: "GIT_PREVIOUS_COMMIT",
17+
SupportedCI.CIRCLECI: "CIRCLE_RANGE",
18+
SupportedCI.TRAVIS: "TRAVIS_COMMIT_RANGE",
19+
SupportedCI.GITLAB: "CI_COMMIT_BEFORE_SHA",
20+
SupportedCI.GITHUB: "GITHUB_PUSH_BASE_SHA",
21+
SupportedCI.DRONE: "DRONE_COMMIT_BEFORE",
22+
}
13323

134-
if before_sha and before_sha != EMPTY_SHA:
135-
commit_list = get_list_commit_SHA(f"{before_sha}~1...")
136-
if commit_list:
137-
return commit_list
24+
CI_COMMIT_VAR = {
25+
SupportedCI.JENKINS: "GIT_COMMIT",
26+
SupportedCI.CIRCLECI: "CIRCLE_SHA1",
27+
SupportedCI.TRAVIS: "TRAVIS_COMMIT",
28+
SupportedCI.GITLAB: "CI_COMMIT_SHA",
29+
SupportedCI.GITHUB: "GITHUB_SHA",
30+
SupportedCI.AZURE: "BUILD_SOURCEVERSION",
31+
SupportedCI.BITBUCKET: "BITBUCKET_COMMIT",
32+
}
13833

139-
if merge_request_target_branch and merge_request_target_branch != EMPTY_SHA:
140-
commit_list = get_list_commit_SHA(f"origin/{merge_request_target_branch}...")
141-
if commit_list:
142-
return commit_list
14334

144-
commit_list = get_list_commit_SHA(f"{commit_sha}~1...")
35+
def collect_commit_range_from_ci_env() -> Tuple[List[str], SupportedCI]:
36+
ci_mode = SupportedCI.from_ci_env()
37+
38+
base_commit_var = CI_COMMIT_VAR.get(ci_mode)
39+
base_commit = os.getenv(base_commit_var, "HEAD") if base_commit_var else "HEAD"
40+
41+
ui.display_verbose(f"\tIdentified base commit as {base_commit}")
42+
43+
target_branch_var = CI_TARGET_BRANCH_ASSOC.get(ci_mode)
44+
target_branch = None
45+
if target_branch_var:
46+
target_branch = os.getenv(target_branch_var)
47+
if target_branch and target_branch != EMPTY_SHA:
48+
ui.display_verbose(f"\tIdentified target branch as {target_branch}")
49+
commit_list = get_list_commit_SHA(
50+
f"{get_remote_prefix()}{target_branch}..{base_commit}"
51+
)
52+
if commit_list:
53+
return commit_list, ci_mode
54+
55+
previous_commit = None
56+
previous_commit_var = CI_PREVIOUS_COMMIT_VAR.get(ci_mode)
57+
if previous_commit_var:
58+
previous_commit = os.getenv(previous_commit_var)
59+
if (
60+
previous_commit is None or previous_commit == EMPTY_SHA
61+
) and ci_mode == SupportedCI.GITHUB:
62+
previous_commit = os.getenv("GITHUB_DEFAULT_BRANCH")
63+
if (
64+
previous_commit is not None
65+
and previous_commit != EMPTY_SHA
66+
and not previous_commit.startswith("...")
67+
):
68+
ui.display_verbose(
69+
f"\tIdentified previous commit or commit range as {previous_commit}"
70+
)
71+
if ci_mode in [SupportedCI.CIRCLECI, SupportedCI.TRAVIS]:
72+
# for these ci envs, previous_commit is a range of commits
73+
commit_range = previous_commit
74+
elif ci_mode == SupportedCI.GITLAB:
75+
commit_range = f"{previous_commit}~1..{base_commit}"
76+
else:
77+
commit_range = f"{previous_commit}..{base_commit}"
78+
commit_list = get_list_commit_SHA(commit_range)
79+
if commit_list:
80+
return commit_list, ci_mode
81+
82+
commit_list = get_list_commit_SHA(f"{base_commit}~1...")
14583
if commit_list:
146-
return commit_list
147-
148-
raise UnexpectedError(
149-
"Unable to get commit range. Please submit an issue with the following info:\n"
150-
" Repository URL: <Fill if public>\n"
151-
f" CI_MERGE_REQUEST_TARGET_BRANCH_NAME: {merge_request_target_branch}\n"
152-
f" CI_COMMIT_BEFORE_SHA: {before_sha}\n"
153-
f" CI_COMMIT_SHA: {commit_sha}"
154-
)
155-
156-
157-
def github_actions_range() -> List[str]: # pragma: no cover
158-
push_before_sha = github_push_previous_commit_sha()
159-
push_base_sha = os.getenv("GITHUB_PUSH_BASE_SHA")
160-
pull_req_base_sha = github_pull_request_previous_commit_sha()
161-
default_branch = os.getenv("GITHUB_DEFAULT_BRANCH")
162-
head_sha = os.getenv("GITHUB_SHA", "HEAD")
163-
164-
ui.display_verbose(
165-
f"github_push_before_sha: {push_before_sha}\n"
166-
f"github_push_base_sha: {push_base_sha}\n"
167-
f"github_pull_base_sha: {pull_req_base_sha}\n"
168-
f"github_default_branch: {default_branch}\n"
169-
f"github_head_sha: {head_sha}"
170-
)
171-
172-
# The PR base sha has to be checked before the push_before_sha
173-
# because the first one is only populated in case of PR
174-
# whereas push_before_sha can be populated in PR in case of
175-
# push force event in a PR
176-
if pull_req_base_sha and pull_req_base_sha != EMPTY_SHA:
177-
commit_list = get_list_commit_SHA(f"{pull_req_base_sha}..")
178-
if commit_list:
179-
return commit_list
180-
181-
if push_before_sha and push_before_sha != EMPTY_SHA:
182-
commit_list = get_list_commit_SHA(f"{push_before_sha}...")
183-
if commit_list:
184-
return commit_list
185-
186-
if push_base_sha and push_base_sha != "null":
187-
commit_list = get_list_commit_SHA(f"{push_base_sha}...")
188-
if commit_list:
189-
return commit_list
190-
191-
if default_branch:
192-
commit_list = get_list_commit_SHA(f"{default_branch}...")
193-
if commit_list:
194-
return commit_list
195-
196-
if head_sha:
197-
commit_list = get_list_commit_SHA(f"{head_sha}~1...")
198-
if commit_list:
199-
return commit_list
200-
201-
raise UnexpectedError(
202-
"Unable to get commit range. Please submit an issue with the following info:\n"
203-
" Repository URL: <Fill if public>\n"
204-
f"github_push_before_sha: {push_before_sha}\n"
205-
f"github_push_base_sha: {push_base_sha}\n"
206-
f"github_pull_base_sha: {pull_req_base_sha}\n"
207-
f"github_default_branch: {default_branch}\n"
208-
f"github_head_sha: {head_sha}"
209-
)
210-
211-
212-
def drone_range() -> List[str]: # pragma: no cover
213-
before_sha = os.getenv("DRONE_COMMIT_BEFORE")
214-
215-
ui.display_verbose(f"DRONE_COMMIT_BEFORE: {before_sha}\n")
216-
217-
if before_sha and before_sha != EMPTY_SHA:
218-
commit_list = get_list_commit_SHA(f"{before_sha}..")
219-
if commit_list:
220-
return commit_list
221-
222-
raise UnexpectedError(
223-
"Unable to get commit range. Please submit an issue with the following info:\n"
224-
" Repository URL: <Fill if public>\n"
225-
f" DRONE_COMMIT_BEFORE: {before_sha}"
226-
)
227-
228-
229-
def azure_range() -> List[str]: # pragma: no cover
230-
head_commit = os.getenv("BUILD_SOURCEVERSION")
231-
232-
ui.display_verbose(f"BUILD_SOURCEVERSION: {head_commit}\n")
233-
234-
if head_commit:
235-
commit_list = get_list_commit_SHA(f"{head_commit}~1...")
236-
if commit_list:
237-
return commit_list
84+
return commit_list, ci_mode
23885

23986
raise UnexpectedError(
24087
"Unable to get commit range. Please submit an issue with the following info:\n"
24188
" Repository URL: <Fill if public>\n"
242-
f" BUILD_SOURCEVERSION: {head_commit}"
89+
f" CI_TYPE: {ci_mode.value}\n"
90+
f" TARGET_BRANCH: {target_branch}\n"
91+
f" PREVIOUS_COMMIT: {previous_commit}\n"
92+
f" BASE_COMMIT: {base_commit}"
24393
)
244-
245-
246-
COLLECT_COMMIT_RANGE_FUNCTIONS = {
247-
SupportedCI.AZURE: azure_range,
248-
SupportedCI.DRONE: drone_range,
249-
SupportedCI.GITHUB: github_actions_range,
250-
SupportedCI.GITLAB: gitlab_ci_range,
251-
SupportedCI.CIRCLECI: circle_ci_range,
252-
SupportedCI.BITBUCKET: bitbucket_pipelines_range,
253-
SupportedCI.TRAVIS: travis_range,
254-
SupportedCI.JENKINS: jenkins_range,
255-
}

ggshield/core/git_hooks/ci/get_scan_ci_parameters.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ def travis_scan_ci_args() -> Tuple[str, str]:
3232
}
3333

3434

35+
def get_remote_prefix(wd: Optional[Union[str, Path]] = None) -> str:
36+
remotes = get_remotes(wd=wd)
37+
if len(remotes) == 0:
38+
# note: this should not happen in practice, esp. in a CI job
39+
ui.display_verbose("\tNo remote found.")
40+
return ""
41+
else:
42+
ui.display_verbose(f"\tUsing first remote {remotes[0]}.")
43+
return f"{remotes[0]}/"
44+
45+
3546
def get_scan_ci_parameters(
3647
current_ci: SupportedCI, wd: Optional[Union[str, Path]] = None
3748
) -> Union[Tuple[str, str], None]:
@@ -50,15 +61,7 @@ def get_scan_ci_parameters(
5061
if current_ci == SupportedCI.TRAVIS:
5162
return travis_scan_ci_args()
5263

53-
remotes = get_remotes(wd=wd)
54-
if len(remotes) == 0:
55-
# note: this should not happen in practice, esp. in a CI job
56-
ui.display_verbose("\tNo remote found.")
57-
remote_prefix = ""
58-
else:
59-
ui.display_verbose(f"\tUsing first remote {remotes[0]}.")
60-
remote_prefix = f"{remotes[0]}/"
61-
64+
remote_prefix = get_remote_prefix(wd=wd)
6265
target_branch_var = CI_TARGET_BRANCH_ASSOC.get(current_ci)
6366
if not target_branch_var:
6467
raise UnexpectedError(f"Using scan ci is not supported for {current_ci.value}.")

0 commit comments

Comments
 (0)