Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,54 @@ jobs:
exit 1
fi

e2e-sarif:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871
with:
fetch-depth: 0

- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3
with:
python-version: '3.12'

- name: Install CLI from local repo
run: |
python -m pip install --upgrade pip
pip install .

- name: Verify --sarif-reachable-only without --reach exits non-zero
run: |
if socketcli --sarif-reachable-only --api-token dummy 2>&1; then
echo "FAIL: Expected non-zero exit"
exit 1
else
echo "PASS: Exited non-zero as expected"
fi

- name: Run Socket CLI scan with --sarif-file
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: |
set -o pipefail
socketcli \
--target-path tests/e2e/fixtures/simple-npm \
--sarif-file /tmp/results.sarif \
--disable-blocking \
2>&1 | tee /tmp/sarif-output.log

- name: Verify SARIF file is valid
run: |
python3 -c "
import json, sys
with open('/tmp/results.sarif') as f:
data = json.load(f)
assert data['version'] == '2.1.0', f'Invalid version: {data[\"version\"]}'
assert '\$schema' in data, 'Missing \$schema'
count = len(data['runs'][0]['results'])
print(f'PASS: Valid SARIF 2.1.0 with {count} result(s)')
"

e2e-reachability:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -107,3 +155,41 @@ jobs:
cat /tmp/reach-output.log
exit 1
fi

- name: Run scan with --sarif-file (all results)
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: |
socketcli \
--target-path tests/e2e/fixtures/simple-npm \
--reach \
--sarif-file /tmp/sarif-all.sarif \
--disable-blocking \
2>/dev/null || true

- name: Run scan with --sarif-file --sarif-reachable-only (filtered results)
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: |
socketcli \
--target-path tests/e2e/fixtures/simple-npm \
--reach \
--sarif-file /tmp/sarif-reachable.sarif \
--sarif-reachable-only \
--disable-blocking \
2>/dev/null || true

- name: Verify reachable-only results are a subset of all results
run: |
python3 -c "
import json
with open('/tmp/sarif-all.sarif') as f:
all_data = json.load(f)
with open('/tmp/sarif-reachable.sarif') as f:
reach_data = json.load(f)
all_count = len(all_data['runs'][0]['results'])
reach_count = len(reach_data['runs'][0]['results'])
print(f'All results: {all_count}, Reachable-only results: {reach_count}')
assert reach_count <= all_count, f'FAIL: reachable ({reach_count}) > all ({all_count})'
print('PASS: Reachable-only results is a subset of all results')
"
50 changes: 50 additions & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Unit Tests

env:
PYTHON_VERSION: "3.12"

on:
push:
branches: [main]
paths:
- "socketsecurity/**/*.py"
- "tests/unit/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/python-tests.yml"
pull_request:
paths:
- "socketsecurity/**/*.py"
- "tests/unit/**/*.py"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/python-tests.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: python-tests-${{ github.ref }}
cancel-in-progress: true

jobs:
python-tests:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871
with:
fetch-depth: 1
persist-credentials: false
- name: 🐍 setup python
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: 🛠️ install deps
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra test
- name: 🧪 run tests
run: uv run pytest -q tests/unit/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ run_container.sh
bin
scripts/*.py
*.json
*.sarif
!tests/**/*.json
markdown_overview_temp.md
markdown_security_temp.md
Expand Down
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,18 +94,27 @@ This will:
- Save to `gl-dependency-scanning-report.json`
- Include all actionable security alerts (error/warn level)

**Save SARIF report to file (e.g. for GitHub Code Scanning, SonarQube, or VS Code):**
```bash
socketcli --sarif-file results.sarif \
--repo owner/repo \
--target-path .
```

**Multiple output formats:**
```bash
socketcli --enable-json \
--enable-sarif \
--sarif-file results.sarif \
--enable-gitlab-security \
--repo owner/repo
```

This will simultaneously generate:
- JSON output to console
- SARIF format to console
- GitLab Security Dashboard report to file
- SARIF report to `results.sarif` (and stdout)
- GitLab Security Dashboard report to `gl-dependency-scanning-report.json`

> **Note:** `--enable-sarif` prints SARIF to stdout only. Use `--sarif-file <path>` to save to a file (this also implies `--enable-sarif`). Add `--sarif-reachable-only` (requires `--reach`) to filter results down to only reachable findings — useful for uploading to GitHub Code Scanning without noisy alerts on unreachable vulns. These flags are independent from `--enable-gitlab-security`, which produces a separate GitLab-specific Dependency Scanning report.

### Requirements

Expand All @@ -121,7 +130,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--
[--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST]
[--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME]
[--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug]
[--enable-json] [--enable-sarif] [--enable-gitlab-security] [--gitlab-security-file <path>]
[--enable-json] [--enable-sarif] [--sarif-file <path>] [--sarif-reachable-only] [--enable-gitlab-security] [--gitlab-security-file <path>]
[--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue]
[--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders]
[--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT]
Expand Down Expand Up @@ -189,7 +198,9 @@ If you don't want to provide the Socket API Token every time then you can use th
| --generate-license | False | False | Generate license information |
| --enable-debug | False | False | Enable debug logging |
| --enable-json | False | False | Output in JSON format |
| --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format |
| --enable-sarif | False | False | Enable SARIF output of results instead of table or JSON format (prints to stdout) |
| --sarif-file | False | | Output file path for SARIF report (implies --enable-sarif). Use this to save SARIF output to a file for upload to GitHub Code Scanning, SonarQube, VS Code, or other SARIF-compatible tools |
| --sarif-reachable-only | False | False | Filter SARIF output to only include reachable findings (requires --reach) |
| --enable-gitlab-security | False | False | Enable GitLab Security Dashboard output format (Dependency Scanning report) |
| --gitlab-security-file | False | gl-dependency-scanning-report.json | Output file path for GitLab Security report |
| --disable-overview | False | False | Disable overview output |
Expand Down Expand Up @@ -725,13 +736,13 @@ socketcli --enable-gitlab-security --gitlab-security-file custom-path.json
GitLab security reports can be generated alongside other output formats:

```bash
socketcli --enable-json --enable-gitlab-security --enable-sarif
socketcli --enable-json --enable-gitlab-security --sarif-file results.sarif
```

This command will:
- Output JSON format to console
- Save GitLab Security Dashboard report to `gl-dependency-scanning-report.json`
- Save SARIF report (if configured)
- Save SARIF report to `results.sarif`

### Security Dashboard Features

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.2.75"
version = "2.2.76"
requires-python = ">= 3.10"
license = {"file" = "LICENSE"}
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = 'socket.dev'
__version__ = '2.2.75'
__version__ = '2.2.76'
USER_AGENT = f'SocketPythonCLI/{__version__}'
26 changes: 26 additions & 0 deletions socketsecurity/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class CliConfig:
allow_unverified: bool = False
enable_json: bool = False
enable_sarif: bool = False
sarif_file: Optional[str] = None
sarif_reachable_only: bool = False
enable_gitlab_security: bool = False
gitlab_security_file: Optional[str] = None
disable_overview: bool = False
Expand Down Expand Up @@ -103,6 +105,10 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
args.api_token
)

# --sarif-file implies --enable-sarif
if args.sarif_file:
args.enable_sarif = True

# Strip quotes from commit message if present
commit_message = args.commit_message
if commit_message and commit_message.startswith('"') and commit_message.endswith('"'):
Expand All @@ -126,6 +132,8 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
'allow_unverified': args.allow_unverified,
'enable_json': args.enable_json,
'enable_sarif': args.enable_sarif,
'sarif_file': args.sarif_file,
'sarif_reachable_only': args.sarif_reachable_only,
'enable_gitlab_security': args.enable_gitlab_security,
'gitlab_security_file': args.gitlab_security_file,
'disable_overview': args.disable_overview,
Expand Down Expand Up @@ -204,6 +212,11 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
logging.error("--workspace-name requires --sub-path to be specified")
exit(1)

# Validate that sarif_reachable_only requires reach
if args.sarif_reachable_only and not args.reach:
logging.error("--sarif-reachable-only requires --reach to be specified")
exit(1)

# Validate that only_facts_file requires reach
if args.only_facts_file and not args.reach:
logging.error("--only-facts-file requires --reach to be specified")
Expand Down Expand Up @@ -471,6 +484,19 @@ def create_argument_parser() -> argparse.ArgumentParser:
action="store_true",
help="Enable SARIF output of results instead of table or JSON format"
)
output_group.add_argument(
"--sarif-file",
dest="sarif_file",
metavar="<path>",
default=None,
help="Output file path for SARIF report (implies --enable-sarif)"
)
output_group.add_argument(
"--sarif-reachable-only",
dest="sarif_reachable_only",
action="store_true",
help="Filter SARIF output to only include reachable findings (requires --reach)"
)
output_group.add_argument(
"--enable-gitlab-security",
dest="enable_gitlab_security",
Expand Down
32 changes: 32 additions & 0 deletions socketsecurity/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,20 @@ def handle_output(self, diff_report: Diff) -> None:
slack_url = "Not configured"
if self.config.slack_plugin.config and self.config.slack_plugin.config.get("url"):
slack_url = self.config.slack_plugin.config.get("url")
slack_mode = (self.config.slack_plugin.config or {}).get("mode", "webhook")
bot_token = os.getenv("SOCKET_SLACK_BOT_TOKEN")
bot_token_status = "Set" if bot_token else "Not set"
self.logger.debug("=== Slack Webhook Debug Information ===")
self.logger.debug(f"Slack Plugin Enabled: {self.config.slack_plugin.enabled}")
self.logger.debug(f"Slack Mode: {slack_mode}")
self.logger.debug(f"SOCKET_SLACK_ENABLED environment variable: {slack_enabled_env}")
self.logger.debug(f"SOCKET_SLACK_CONFIG_JSON environment variable: {slack_config_env}")
self.logger.debug(f"Slack Webhook URL: {slack_url}")
self.logger.debug(f"SOCKET_SLACK_BOT_TOKEN: {bot_token_status}")
self.logger.debug(f"Slack Alert Levels: {self.config.slack_plugin.levels}")
if self.config.reach:
facts_path = os.path.join(self.config.target_path or ".", self.config.reach_output_file or ".socket.facts.json")
self.logger.debug(f"Reachability facts file: {facts_path} (exists: {os.path.exists(facts_path)})")
self.logger.debug("=====================================")

if self.config.slack_plugin.enabled:
Expand Down Expand Up @@ -139,14 +147,38 @@ def output_console_json(self, diff_report: Diff, sbom_file_name: Optional[str] =
def output_console_sarif(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None:
"""
Generate SARIF output from the diff report and print to console.
If --sarif-file is configured, also save to file.
If --sarif-reachable-only is set, filters to blocking (reachable) alerts only.
"""
if diff_report.id != "NO_DIFF_RAN":
# When --sarif-reachable-only is set, filter to error=True alerts only.
# This mirrors the Slack plugin's reachability_alerts_only behaviou:
# when --reach is used, error=True reflects Socket's reachability-aware policy.
if self.config.sarif_reachable_only:
filtered_alerts = [a for a in diff_report.new_alerts if getattr(a, "error", False)]
diff_report = Diff(
new_alerts=filtered_alerts,
diff_url=getattr(diff_report, "diff_url", ""),
new_packages=getattr(diff_report, "new_packages", []),
removed_packages=getattr(diff_report, "removed_packages", []),
packages=getattr(diff_report, "packages", {}),
)
diff_report.id = "filtered"

# Generate the SARIF structure using Messages
console_security_comment = Messages.create_security_comment_sarif(diff_report)
self.save_sbom_file(diff_report, sbom_file_name)
# Print the SARIF output to the console in JSON format
print(json.dumps(console_security_comment, indent=2))

# Save to file if --sarif-file is specified
if self.config.sarif_file:
sarif_path = Path(self.config.sarif_file)
sarif_path.parent.mkdir(parents=True, exist_ok=True)
with open(sarif_path, "w") as f:
json.dump(console_security_comment, f, indent=2)
self.logger.info(f"SARIF report saved to {self.config.sarif_file}")

def report_pass(self, diff_report: Diff) -> bool:
"""Determines if the report passes security checks"""
# Priority 1: --disable-blocking always passes
Expand Down
Loading
Loading