Skip to content

Commit 85d950a

Browse files
committed
CM-52972 - Add pre-push hook support
1 parent b5b2591 commit 85d950a

File tree

10 files changed

+603
-14
lines changed

10 files changed

+603
-14
lines changed

.pre-commit-hooks.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,24 @@
1616
language_version: python3
1717
entry: cycode
1818
args: [ '-o', 'text', '--no-progress-meter', 'scan', '-t', 'sast', 'pre-commit' ]
19+
- id: cycode-pre-push
20+
name: Cycode Secrets pre-push defender
21+
language: python
22+
language_version: python3
23+
entry: cycode
24+
args: [ '-o', 'text', '--no-progress-meter', 'scan', '-t', 'secret', 'pre-push' ]
25+
stages: [pre-push]
26+
- id: cycode-sca-pre-push
27+
name: Cycode SCA pre-push defender
28+
language: python
29+
language_version: python3
30+
entry: cycode
31+
args: [ '-o', 'text', '--no-progress-meter', 'scan', '-t', 'sca', 'pre-push' ]
32+
stages: [pre-push]
33+
- id: cycode-sast-pre-push
34+
name: Cycode SAST pre-push defender
35+
language: python
36+
language_version: python3
37+
entry: cycode
38+
args: [ '-o', 'text', '--no-progress-meter', 'scan', '-t', 'sast', 'pre-push' ]
39+
stages: [pre-push]

README.md

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This guide walks you through both installation and usage.
3737
4. [Commit History Scan](#commit-history-scan)
3838
1. [Commit Range Option (Diff Scanning)](#commit-range-option-diff-scanning)
3939
5. [Pre-Commit Scan](#pre-commit-scan)
40+
6. [Pre-Push Scan](#pre-push-scan)
4041
2. [Scan Results](#scan-results)
4142
1. [Show/Hide Secrets](#showhide-secrets)
4243
2. [Soft Fail](#soft-fail)
@@ -213,13 +214,15 @@ export CYCODE_CLIENT_SECRET={your Cycode Secret Key}
213214

214215
## Install Pre-Commit Hook
215216

216-
Cycodes pre-commit hook can be set up within your local repository so that the Cycode CLI application will identify any issues with your code automatically before you commit it to your codebase.
217+
Cycode's pre-commit and pre-push hooks can be set up within your local repository so that the Cycode CLI application will identify any issues with your code automatically before you commit or push it to your codebase.
217218
218219
> [!NOTE]
219-
> pre-commit hook is not available for IaC scans.
220+
> pre-commit and pre-push hooks are not available for IaC scans.
220221
221222
Perform the following steps to install the pre-commit hook:
222223
224+
### Installing Pre-Commit Hook
225+
223226
1. Install the pre-commit framework (Python 3.9 or higher must be installed):
224227
225228
```bash
@@ -278,6 +281,37 @@ Perform the following steps to install the pre-commit hook:
278281
> Trigger happens on `git commit` command.
279282
> Hook triggers only on the files that are staged for commit.
280283
284+
### Installing Pre-Push Hook
285+
286+
To install the pre-push hook in addition to or instead of the pre-commit hook:
287+
288+
1. Add the pre-push hooks to your `.pre-commit-config.yaml` file:
289+
290+
```yaml
291+
repos:
292+
- repo: https://github.com/cycodehq/cycode-cli
293+
rev: v3.4.2
294+
hooks:
295+
- id: cycode-pre-push
296+
stages: [pre-push]
297+
```
298+
299+
2. Install the pre-push hook:
300+
301+
```bash
302+
pre-commit install --hook-type pre-push
303+
```
304+
305+
3. For both pre-commit and pre-push hooks, use:
306+
307+
```bash
308+
pre-commit install
309+
pre-commit install --hook-type pre-push
310+
```
311+
312+
> [!NOTE]
313+
> Pre-push hooks trigger on `git push` command and scan only the commits about to be pushed.
314+
281315
# Cycode CLI Commands
282316
283317
The following are the options and commands available with the Cycode CLI application:
@@ -786,6 +820,91 @@ After installing the pre-commit hook, you may occasionally wish to skip scanning
786820
SKIP=cycode git commit -m <your commit message>`
787821
```
788822
823+
### Pre-Push Scan
824+
825+
A pre-push scan automatically identifies any issues before you push changes to the remote repository. This hook runs on the client side and scans only the commits that are about to be pushed, making it efficient for catching issues before they reach the remote repository.
826+
827+
> [!NOTE]
828+
> Pre-push hook is not available for IaC scans.
829+
830+
The pre-push hook integrates with the pre-commit framework and can be configured to run before any `git push` operation.
831+
832+
#### Installing Pre-Push Hook
833+
834+
To set up the pre-push hook using the pre-commit framework:
835+
836+
1. Install the pre-commit framework (if not already installed):
837+
838+
```bash
839+
pip3 install pre-commit
840+
```
841+
842+
2. Create or update your `.pre-commit-config.yaml` file to include the pre-push hooks:
843+
844+
```yaml
845+
repos:
846+
- repo: https://github.com/cycodehq/cycode-cli
847+
rev: v3.4.2
848+
hooks:
849+
- id: cycode-pre-push
850+
stages: [pre-push]
851+
```
852+
853+
3. For multiple scan types, use this configuration:
854+
855+
```yaml
856+
repos:
857+
- repo: https://github.com/cycodehq/cycode-cli
858+
rev: v3.4.2
859+
hooks:
860+
- id: cycode-pre-push # Secrets scan
861+
stages: [pre-push]
862+
- id: cycode-sca-pre-push # SCA scan
863+
stages: [pre-push]
864+
- id: cycode-sast-pre-push # SAST scan
865+
stages: [pre-push]
866+
```
867+
868+
4. Install the pre-push hook:
869+
870+
```bash
871+
pre-commit install --hook-type pre-push
872+
```
873+
874+
A successful installation will result in the message: `Pre-push installed at .git/hooks/pre-push`.
875+
876+
5. Keep the pre-push hook up to date:
877+
878+
```bash
879+
pre-commit autoupdate
880+
```
881+
882+
#### How Pre-Push Scanning Works
883+
884+
The pre-push hook:
885+
- Receives information about what commits are being pushed
886+
- Calculates the appropriate commit range to scan
887+
- For new branches: scans all commits from the merge base with the default branch
888+
- For existing branches: scans only the new commits since the last push
889+
- Runs the same comprehensive scanning as other Cycode scan modes
890+
891+
#### Skipping Pre-Push Scans
892+
893+
To skip the pre-push scan for a specific push operation, use:
894+
895+
```bash
896+
SKIP=cycode-pre-push git push
897+
```
898+
899+
Or to skip all pre-push hooks:
900+
901+
```bash
902+
git push --no-verify
903+
```
904+
905+
> [!TIP]
906+
> The pre-push hook is triggered on `git push` command and scans only the commits that are about to be pushed, making it more efficient than scanning the entire repository.
907+
789908
## Scan Results
790909
791910
Each scan will complete with a message stating if any issues were found or not.

cycode/cli/apps/scan/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
from cycode.cli.apps.scan.commit_history.commit_history_command import commit_history_command
44
from cycode.cli.apps.scan.path.path_command import path_command
55
from cycode.cli.apps.scan.pre_commit.pre_commit_command import pre_commit_command
6+
from cycode.cli.apps.scan.pre_push.pre_push_command import pre_push_command
67
from cycode.cli.apps.scan.pre_receive.pre_receive_command import pre_receive_command
78
from cycode.cli.apps.scan.repository.repository_command import repository_command
89
from cycode.cli.apps.scan.scan_command import scan_command, scan_command_result_callback
910

1011
app = typer.Typer(name='scan', no_args_is_help=True)
1112

13+
_AUTOMATION_COMMANDS_RICH_HELP_PANEL = 'Automation commands'
14+
1215
_scan_command_docs = 'https://github.com/cycodehq/cycode-cli/blob/main/README.md#scan-command'
1316
_scan_command_epilog = f'[bold]Documentation:[/] [link={_scan_command_docs}]{_scan_command_docs}[/link]'
1417

@@ -26,16 +29,22 @@
2629
app.command(
2730
name='pre-commit',
2831
short_help='Use this command in pre-commit hook to scan any content that was not committed yet.',
29-
rich_help_panel='Automation commands',
32+
rich_help_panel=_AUTOMATION_COMMANDS_RICH_HELP_PANEL,
3033
)(pre_commit_command)
34+
app.command(
35+
name='pre-push',
36+
short_help='Use this command in pre-push hook to scan commits before pushing them to the remote repository.',
37+
rich_help_panel=_AUTOMATION_COMMANDS_RICH_HELP_PANEL,
38+
)(pre_push_command)
3139
app.command(
3240
name='pre-receive',
3341
short_help='Use this command in pre-receive hook '
3442
'to scan commits on the server side before pushing them to the repository.',
35-
rich_help_panel='Automation commands',
43+
rich_help_panel=_AUTOMATION_COMMANDS_RICH_HELP_PANEL,
3644
)(pre_receive_command)
3745

3846
# backward compatibility
3947
app.command(hidden=True, name='commit_history')(commit_history_command)
4048
app.command(hidden=True, name='pre_commit')(pre_commit_command)
49+
app.command(hidden=True, name='pre_push')(pre_push_command)
4150
app.command(hidden=True, name='pre_receive')(pre_receive_command)

cycode/cli/apps/scan/pre_push/__init__.py

Whitespace-only changes.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import logging
2+
import os
3+
from typing import Annotated, Optional
4+
5+
import typer
6+
7+
from cycode.cli import consts
8+
from cycode.cli.apps.scan.commit_range_scanner import (
9+
is_verbose_mode_requested_in_pre_receive_scan,
10+
scan_commit_range,
11+
should_skip_pre_receive_scan,
12+
)
13+
from cycode.cli.config import configuration_manager
14+
from cycode.cli.console import console
15+
from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception
16+
from cycode.cli.files_collector.commit_range_documents import (
17+
calculate_pre_push_commit_range,
18+
parse_pre_push_input,
19+
)
20+
from cycode.cli.logger import logger
21+
from cycode.cli.utils import scan_utils
22+
from cycode.cli.utils.sentry import add_breadcrumb
23+
from cycode.cli.utils.task_timer import TimeoutAfter
24+
from cycode.logger import set_logging_level
25+
26+
27+
def pre_push_command(
28+
ctx: typer.Context,
29+
_: Annotated[Optional[list[str]], typer.Argument(help='Ignored arguments', hidden=True)] = None,
30+
) -> None:
31+
try:
32+
add_breadcrumb('pre_push')
33+
34+
if should_skip_pre_receive_scan():
35+
logger.info(
36+
'A scan has been skipped as per your request. '
37+
'Please note that this may leave your system vulnerable to secrets that have not been detected.'
38+
)
39+
return
40+
41+
if is_verbose_mode_requested_in_pre_receive_scan():
42+
ctx.obj['verbose'] = True
43+
set_logging_level(logging.DEBUG)
44+
logger.debug('Verbose mode enabled: all log levels will be displayed.')
45+
46+
command_scan_type = ctx.info_name
47+
timeout = configuration_manager.get_pre_push_command_timeout(command_scan_type)
48+
with TimeoutAfter(timeout):
49+
push_update_details = parse_pre_push_input()
50+
commit_range = calculate_pre_push_commit_range(push_update_details)
51+
if not commit_range:
52+
logger.info(
53+
'No new commits found for pushed branch, %s',
54+
{'push_update_details': push_update_details},
55+
)
56+
return
57+
58+
scan_commit_range(
59+
ctx=ctx,
60+
repo_path=os.getcwd(),
61+
commit_range=commit_range,
62+
max_commits_count=configuration_manager.get_pre_push_max_commits_to_scan_count(command_scan_type),
63+
)
64+
65+
if scan_utils.is_scan_failed(ctx):
66+
console.print(consts.PRE_RECEIVE_AND_PUSH_REMEDIATION_MESSAGE)
67+
except Exception as e:
68+
handle_scan_exception(ctx, e)

cycode/cli/apps/scan/pre_receive/pre_receive_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,6 @@ def pre_receive_command(
6363
)
6464

6565
if scan_utils.is_scan_failed(ctx):
66-
console.print(consts.PRE_RECEIVE_REMEDIATION_MESSAGE)
66+
console.print(consts.PRE_RECEIVE_AND_PUSH_REMEDIATION_MESSAGE)
6767
except Exception as e:
6868
handle_scan_exception(ctx, e)

cycode/cli/consts.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,13 @@
240240
DEFAULT_PRE_RECEIVE_MAX_COMMITS_TO_SCAN_COUNT = 50
241241
PRE_RECEIVE_COMMAND_TIMEOUT_ENV_VAR_NAME = 'PRE_RECEIVE_COMMAND_TIMEOUT'
242242
DEFAULT_PRE_RECEIVE_COMMAND_TIMEOUT_IN_SECONDS = 60
243-
PRE_RECEIVE_REMEDIATION_MESSAGE = """
243+
# pre push scan
244+
PRE_PUSH_MAX_COMMITS_TO_SCAN_COUNT_ENV_VAR_NAME = 'PRE_PUSH_MAX_COMMITS_TO_SCAN_COUNT'
245+
DEFAULT_PRE_PUSH_MAX_COMMITS_TO_SCAN_COUNT = 50
246+
PRE_PUSH_COMMAND_TIMEOUT_ENV_VAR_NAME = 'PRE_PUSH_COMMAND_TIMEOUT'
247+
DEFAULT_PRE_PUSH_COMMAND_TIMEOUT_IN_SECONDS = 60
248+
# pre push and pre receive common
249+
PRE_RECEIVE_AND_PUSH_REMEDIATION_MESSAGE = """
244250
Cycode Secrets Push Protection
245251
------------------------------------------------------------------------------
246252
Resolve the following secrets by rewriting your local commit history before pushing again.

cycode/cli/files_collector/commit_range_documents.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,69 @@ def parse_pre_receive_input() -> str:
211211
return pre_receive_input.splitlines()[0]
212212

213213

214+
def parse_pre_push_input() -> str:
215+
"""Parse input to pre-push hook details.
216+
217+
Example input:
218+
local_ref local_object_name remote_ref remote_object_name
219+
---------------------------------------------------------
220+
refs/heads/main 9cf90954ef26e7c58284f8ebf7dcd0fcf711152a refs/heads/main 973a96d3e925b65941f7c47fa16129f1577d499f
221+
refs/heads/feature-branch 3378e52dcfa47fb11ce3a4a520bea5f85d5d0bf3 refs/heads/feature-branch 59564ef68745bca38c42fc57a7822efd519a6bd9
222+
223+
:return: First, push update details (input's first line)
224+
""" # noqa: E501
225+
pre_push_input = sys.stdin.read().strip()
226+
if not pre_push_input:
227+
raise ValueError(
228+
'Pre push input was not found. Make sure that you are using this command only in pre-push hook'
229+
)
230+
231+
# each line represents a branch push request, handle the first one only
232+
return pre_push_input.splitlines()[0]
233+
234+
235+
def calculate_pre_push_commit_range(push_update_details: str) -> Optional[str]:
236+
"""Calculate the commit range for pre-push hook scanning.
237+
238+
Args:
239+
push_update_details: String in format "local_ref local_object_name remote_ref remote_object_name"
240+
241+
Returns:
242+
Commit range string for scanning, or None if no scanning is needed
243+
"""
244+
local_ref, local_object_name, remote_ref, remote_object_name = push_update_details.split()
245+
246+
if remote_object_name == consts.EMPTY_COMMIT_SHA:
247+
try:
248+
repo = git_proxy.get_repo(os.getcwd())
249+
default_branches = ['origin/main', 'origin/master', 'main', 'master']
250+
251+
merge_base = None
252+
for default_branch in default_branches:
253+
try:
254+
merge_base = repo.git.merge_base(local_object_name, default_branch)
255+
break
256+
except Exception as e:
257+
logger.debug('Failed to find merge base with %s: %s', default_branch, exc_info=e)
258+
continue
259+
260+
if merge_base:
261+
return f'{merge_base}..{local_object_name}'
262+
263+
logger.debug('Failed to find merge base with any default branch')
264+
return '--all'
265+
except Exception as e:
266+
logger.debug('Failed to get repo for pre-push commit range calculation: %s', exc_info=e)
267+
return '--all'
268+
269+
# If deleting a branch (local_object_name is all zeros), no need to scan
270+
if local_object_name == consts.EMPTY_COMMIT_SHA:
271+
return None
272+
273+
# For updates to existing branches, scan from remote to local
274+
return f'{remote_object_name}..{local_object_name}'
275+
276+
214277
def get_diff_file_path(diff: 'Diff', relative: bool = False, repo: Optional['Repo'] = None) -> Optional[str]:
215278
"""Get the file path from a git Diff object.
216279

0 commit comments

Comments
 (0)