Skip to content
Open
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
216 changes: 57 additions & 159 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: E2E Test
name: E2E Tests

on:
push:
Expand All @@ -10,93 +10,58 @@ permissions:
contents: read

jobs:
e2e-scan:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871
with:
fetch-depth: 0
persist-credentials: false

- 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: Run Socket CLI scan
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: |
set -o pipefail
socketcli \
--target-path tests/e2e/fixtures/simple-npm \
--disable-blocking \
--enable-debug \
2>&1 | tee /tmp/scan-output.log

- name: Verify scan produced a report
run: |
if grep -q "Full scan report URL: https://socket.dev/" /tmp/scan-output.log; then
echo "PASS: Full scan report URL found"
grep "Full scan report URL:" /tmp/scan-output.log
elif grep -q "Diff Url: https://socket.dev/" /tmp/scan-output.log; then
echo "PASS: Diff URL found"
grep "Diff Url:" /tmp/scan-output.log
else
echo "FAIL: No report URL found in scan output"
cat /tmp/scan-output.log
exit 1
fi

e2e-sarif:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871
with:
fetch-depth: 0
persist-credentials: false

- 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: 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:
e2e:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- name: scan
args: >-
--target-path tests/e2e/fixtures/simple-npm
--disable-blocking
--enable-debug
validate: tests/e2e/validate-scan.sh

- name: sarif
args: >-
--target-path tests/e2e/fixtures/simple-npm
--sarif-file /tmp/results.sarif
--disable-blocking
validate: tests/e2e/validate-sarif.sh

- name: reachability
args: >-
--target-path tests/e2e/fixtures/simple-npm
--reach
--disable-blocking
--enable-debug
validate: tests/e2e/validate-reachability.sh
setup-node: "true"

- name: gitlab
args: >-
--target-path tests/e2e/fixtures/simple-npm
--enable-gitlab-security
--disable-blocking
validate: tests/e2e/validate-gitlab.sh

- name: json
args: >-
--target-path tests/e2e/fixtures/simple-npm
--enable-json
--disable-blocking
validate: tests/e2e/validate-json.sh

- name: pypi
args: >-
--target-path tests/e2e/fixtures/simple-pypi
--disable-blocking
--enable-debug
validate: tests/e2e/validate-scan.sh

name: e2e-${{ matrix.name }}
steps:
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871
with:
Expand All @@ -108,6 +73,7 @@ jobs:
python-version: '3.12'

- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
if: matrix.setup-node == 'true'
with:
node-version: '20'

Expand All @@ -117,85 +83,17 @@ jobs:
pip install .

- name: Install uv
if: matrix.setup-node == 'true'
run: pip install uv

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

- name: Verify reachability analysis completed
run: |
if grep -q "Reachability analysis completed successfully" /tmp/reach-output.log; then
echo "PASS: Reachability analysis completed"
grep "Reachability analysis completed successfully" /tmp/reach-output.log
grep "Results written to:" /tmp/reach-output.log || true
else
echo "FAIL: Reachability analysis did not complete successfully"
cat /tmp/reach-output.log
exit 1
fi

- name: Verify scan produced a report
run: |
if grep -q "Full scan report URL: https://socket.dev/" /tmp/reach-output.log; then
echo "PASS: Full scan report URL found"
grep "Full scan report URL:" /tmp/reach-output.log
elif grep -q "Diff Url: https://socket.dev/" /tmp/reach-output.log; then
echo "PASS: Diff URL found"
grep "Diff Url:" /tmp/reach-output.log
else
echo "FAIL: No report URL found in scan output"
cat /tmp/reach-output.log
exit 1
fi
socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log

- name: Run scan with --sarif-file (all results)
- name: Validate 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 \
--sarif-scope full \
--sarif-reachability all \
--disable-blocking \
2>/dev/null

- name: Run scan with --sarif-file --sarif-reachability reachable (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-scope full \
--sarif-reachability reachable \
--disable-blocking \
2>/dev/null

- name: Verify reachable-only results are a subset of all results
run: |
test -f /tmp/sarif-all.sarif
test -f /tmp/sarif-reachable.sarif
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')
"
run: bash ${{ matrix.validate }}
14 changes: 11 additions & 3 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,15 +702,22 @@ All alert types are included in the GitLab report if they're marked as `error` o

### Report Schema

Socket CLI generates reports compliant with [GitLab Dependency Scanning schema version 15.0.0](https://docs.gitlab.com/ee/development/integrations/secure.html). The reports include:
Socket CLI generates reports compliant with [GitLab Dependency Scanning schema version 15.0.0](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/v15.0.0/dist/dependency-scanning-report-format.json). The reports include:

- **Scan metadata**: Analyzer and scanner information
- **Scan metadata**: Analyzer and scanner information with ISO 8601 timestamps
- **Vulnerabilities**: Detailed vulnerability data with:
- Unique deterministic UUIDs for tracking
- Package location and dependency information
- Severity levels mapped from Socket's analysis
- Socket-specific alert types and CVE identifiers
- Links to Socket.dev for detailed analysis
- **Dependency files**: Manifest files and their dependencies discovered during the scan

**Schema compatibility:** The v15.0.0 schema is supported across all GitLab versions 12.0+ (both self-hosted and cloud). The report includes the `dependency_files` field, which is required by v15.0.0 and accepted as an optional extra by newer schema versions, ensuring maximum compatibility across GitLab instances.

### Performance Notes

When `--enable-gitlab-security` (or `--enable-json` / `--enable-sarif`) is used with a full scan (non-diff mode), the CLI fetches package and alert data from the scan results to populate the report. This adds time proportional to the number of packages in the scan. Without these output flags, no additional data is fetched and scan performance is unchanged.

### Requirements

Expand All @@ -726,7 +733,8 @@ Socket CLI generates reports compliant with [GitLab Dependency Scanning schema v
- Ensure the report file follows the correct schema format

**Empty vulnerabilities array:**
- This is normal if no new security issues were detected
- This is normal if no new security issues were detected in diff mode
- For full scans, ensure you are using `--enable-gitlab-security` so alert data is fetched
- Check Socket.dev dashboard for full analysis details

## Development
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.80"
version = "2.2.81"
requires-python = ">= 3.11"
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.80'
__version__ = '2.2.81'
USER_AGENT = f'SocketPythonCLI/{__version__}'
67 changes: 65 additions & 2 deletions socketsecurity/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,9 +659,48 @@ def create_full_scan_with_report_url(
diff.report_url = f"{base_socket}/{self.config.org_slug}/sbom/{new_full_scan.id}"
diff.diff_url = diff.report_url
diff.id = new_full_scan.id
diff.packages = {}

# Return result in the format expected by the user
needs_alerts = (
self.cli_config is not None
and (
self.cli_config.enable_gitlab_security
or self.cli_config.enable_json
or self.cli_config.enable_sarif
)
)

if needs_alerts:
log.info("Output format requires alerts, fetching SBOM data for full scan")
sbom_start = time.time()
sbom_artifacts_dict = self.get_sbom_data(new_full_scan.id)
sbom_artifacts = self.get_sbom_data_list(sbom_artifacts_dict)
packages = self._create_packages_dict_without_license_text(sbom_artifacts)
diff.packages = packages

all_alerts_collection: Dict[str, List[Issue]] = {}
for package_id, package in packages.items():
self.add_package_alerts_to_collection(
package=package,
alerts_collection=all_alerts_collection,
packages=packages
)

consolidated: Set[str] = set()
for alert_key, alerts in all_alerts_collection.items():
for alert in alerts:
alert_str = f"{alert.purl},{alert.type}"
if (alert.error or alert.warn) and alert_str not in consolidated:
diff.new_alerts.append(alert)
consolidated.add(alert_str)

sbom_end = time.time()
log.info(
f"Fetched {len(packages)} packages and {len(diff.new_alerts)} alerts "
f"in {sbom_end - sbom_start:.2f}s"
)
else:
diff.packages = {}

return diff

def get_full_scan(self, full_scan_id: str) -> FullScan:
Expand Down Expand Up @@ -712,6 +751,30 @@ def create_packages_dict(self, sbom_artifacts: list[SocketArtifact]) -> dict[str

return packages

@staticmethod
def _create_packages_dict_without_license_text(
sbom_artifacts: list[SocketArtifact],
) -> dict[str, Package]:
"""Like create_packages_dict but skips the license-metadata API call.

Used when we only need packages for alert extraction (e.g. populating
GitLab/JSON/SARIF reports from a full scan) and don't need license text.
"""
packages: dict[str, Package] = {}
top_level_count: dict[str, int] = {}
for artifact in sbom_artifacts:
package = Package.from_socket_artifact(asdict(artifact))
if package.id not in packages:
packages[package.id] = package
if package.topLevelAncestors:
for top_id in package.topLevelAncestors:
top_level_count[top_id] = top_level_count.get(top_id, 0) + 1

for package_id, package in packages.items():
package.transitives = top_level_count.get(package_id, 0)

return packages

def get_package_license_text(self, package: Package) -> str:
"""
Gets the license text for a package if available.
Expand Down
Loading
Loading