Skip to content

Commit 89b675e

Browse files
committed
Merge main into mucha-dev-gitlab-security-output (using main versions)
2 parents c3e42ac + 54e6ec7 commit 89b675e

File tree

14 files changed

+466
-146
lines changed

14 files changed

+466
-146
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.2.64
4+
5+
- Included PyPy in the Docker image.
6+
37
## 2.2.57
48

59
- Fixed Dockerfile to set `GOROOT` to `/usr/lib/go` when using system Go (`GO_VERSION=system`) instead of always using `/usr/local/go`.

Dockerfile

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ RUN if [ "$DOTNET_VERSION" = "6" ]; then \
5858
echo "Unsupported .NET version: $DOTNET_VERSION. Supported: 6, 8" && exit 1; \
5959
fi
6060

61+
# Install PyPy (Alpine-compatible build for x86_64 only)
62+
# PyPy is an alternative Python interpreter that makes the Python reachability analysis faster.
63+
# This is a custom build of PyPy3.11 for Alpine on x86-64.
64+
ARG TARGETARCH # Passed by Docker buildx
65+
RUN if [ "$TARGETARCH" = "amd64" ]; then \
66+
PYPY_URL="https://github.com/BarrensZeppelin/alpine-pypy/releases/download/alp3.23.1-pypy3.11-7.3.20/pypy3.11-v7.3.20-linux64-alpine3.21.tar.bz2" && \
67+
PYPY_SHA256="60847fea6ffe96f10a3cd4b703686e944bb4fbcc01b7200c044088dd228425e1" && \
68+
curl -L -o /tmp/pypy.tar.bz2 "$PYPY_URL" && \
69+
echo "$PYPY_SHA256 /tmp/pypy.tar.bz2" | sha256sum -c - && \
70+
mkdir -p /opt/pypy && \
71+
tar -xj --strip-components=1 -C /opt/pypy -f /tmp/pypy.tar.bz2 && \
72+
rm /tmp/pypy.tar.bz2 && \
73+
ln -s /opt/pypy/bin/pypy3 /bin/pypy3 && \
74+
pypy3 --version; \
75+
fi
76+
6177
# Install additional tools
6278
RUN npm install @coana-tech/cli socket -g && \
6379
gem install bundler && \
@@ -104,4 +120,4 @@ RUN mkdir -p /go/src && chmod -R 777 /go
104120
COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
105121
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
106122

107-
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
123+
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ The CLI will automatically install @coana-tech/cli if not present. Use `--reach`
222222
|:-------------------------|:---------|:--------|:----------------------------------------------------------------------|
223223
| --ignore-commit-files | False | False | Ignore commit files |
224224
| --disable-blocking | False | False | Disable blocking mode |
225+
| --strict-blocking | False | False | Fail on ANY security policy violations (blocking severity), not just new ones. Only works in diff mode. See [Strict Blocking Mode](#strict-blocking-mode) for details. |
225226
| --enable-diff | False | False | Enable diff mode even when using --integration api (forces diff mode without SCM integration) |
226227
| --scm | False | api | Source control management type |
227228
| --timeout | False | | Timeout in seconds for API requests |
@@ -357,6 +358,99 @@ Bot mode (`bot_configs` array items):
357358
- `alert_types` (array, optional): Only send specific alert types
358359
- `reachability_alerts_only` (boolean, default: false): Only send reachable vulnerabilities when using `--reach`
359360
361+
## Strict Blocking Mode
362+
363+
The `--strict-blocking` flag enforces a zero-tolerance security policy by failing builds when **ANY** security violations with blocking severity exist, not just new ones introduced in the current changes.
364+
365+
### Standard vs Strict Blocking Behavior
366+
367+
**Standard Behavior (Default)**:
368+
- ✅ Passes if no NEW violations are introduced
369+
- ❌ Fails only on NEW violations from your changes
370+
- 🟡 Existing violations are ignored
371+
372+
**Strict Blocking Behavior (`--strict-blocking`)**:
373+
- ✅ Passes only if NO violations exist (new or existing)
374+
- ❌ Fails on ANY violation (new OR existing)
375+
- 🔴 Enforces zero-tolerance policy
376+
377+
### Usage Examples
378+
379+
**Basic strict blocking:**
380+
```bash
381+
socketcli --target-path ./my-project --strict-blocking
382+
```
383+
384+
**In GitLab CI:**
385+
```bash
386+
socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} --strict-blocking
387+
```
388+
389+
**In GitHub Actions:**
390+
```bash
391+
socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER --strict-blocking
392+
```
393+
394+
### Output Differences
395+
396+
**Standard scan output:**
397+
```
398+
Security issues detected by Socket Security:
399+
- NEW blocking issues: 2
400+
- NEW warning issues: 1
401+
```
402+
403+
**Strict blocking scan output:**
404+
```
405+
Security issues detected by Socket Security:
406+
- NEW blocking issues: 2
407+
- NEW warning issues: 1
408+
- EXISTING blocking issues: 5 (causing failure due to --strict-blocking)
409+
- EXISTING warning issues: 3
410+
```
411+
412+
### Use Cases
413+
414+
1. **Zero-Tolerance Security Policy**: Enforce that no security violations exist in your codebase at any time
415+
2. **Gradual Security Improvement**: Use alongside standard scans to monitor existing violations while blocking new ones
416+
3. **Protected Branch Enforcement**: Require all violations to be resolved before merging to main/production
417+
4. **Security Audits**: Scheduled scans that fail if any violations accumulate
418+
419+
### Important Notes
420+
421+
- **Diff Mode Only**: The flag only works in diff mode (with SCM integration). In API mode, a warning is logged.
422+
- **Error-Level Only**: Only fails on `error=True` alerts (blocking severity), not warnings.
423+
- **Priority**: `--disable-blocking` takes precedence - if both flags are set, the build will always pass.
424+
- **First Scan**: On the very first scan of a repository, there are no "existing" violations, so behavior is identical to standard mode.
425+
426+
### Flag Combinations
427+
428+
**Strict blocking with debugging:**
429+
```bash
430+
socketcli --strict-blocking --enable-debug
431+
```
432+
433+
**Strict blocking with JSON output:**
434+
```bash
435+
socketcli --strict-blocking --enable-json > security-report.json
436+
```
437+
438+
**Override for testing** (passes even with violations):
439+
```bash
440+
socketcli --strict-blocking --disable-blocking
441+
```
442+
443+
### Migration Strategy
444+
445+
**Phase 1: Assessment** - Add strict scan with `allow_failure: true` in CI
446+
**Phase 2: Remediation** - Fix or triage all violations
447+
**Phase 3: Enforcement** - Set `allow_failure: false` to block merges
448+
449+
For complete GitLab CI/CD examples, see:
450+
- [`.gitlab-ci-strict-blocking-demo.yml`](.gitlab-ci-strict-blocking-demo.yml) - Comprehensive demo
451+
- [`.gitlab-ci-strict-blocking-production.yml`](.gitlab-ci-strict-blocking-production.yml) - Production-ready template
452+
- [`STRICT-BLOCKING-GITLAB-CI.md`](STRICT-BLOCKING-GITLAB-CI.md) - Full documentation
453+
360454
## Automatic Git Detection
361455
362456
The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.3.0"
9+
version = "2.2.66"
1010
requires-python = ">= 3.10"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.3.0'
2+
__version__ = '2.2.66'
33
USER_AGENT = f'SocketPythonCLI/{__version__}'

socketsecurity/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class CliConfig:
4747
files: str = None
4848
ignore_commit_files: bool = False
4949
disable_blocking: bool = False
50+
strict_blocking: bool = False
5051
integration_type: IntegrationType = "api"
5152
integration_org_slug: Optional[str] = None
5253
pending_head: bool = False
@@ -127,6 +128,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
127128
'files': args.files,
128129
'ignore_commit_files': args.ignore_commit_files,
129130
'disable_blocking': args.disable_blocking,
131+
'strict_blocking': args.strict_blocking,
130132
'integration_type': args.integration,
131133
'pending_head': args.pending_head,
132134
'timeout': args.timeout,
@@ -540,6 +542,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
540542
action="store_true",
541543
help=argparse.SUPPRESS
542544
)
545+
advanced_group.add_argument(
546+
"--strict-blocking",
547+
dest="strict_blocking",
548+
action="store_true",
549+
help="Fail on ANY security policy violations (blocking severity), not just new ones. Only works in diff mode."
550+
)
543551
advanced_group.add_argument(
544552
"--enable-diff",
545553
dest="enable_diff",

socketsecurity/core/__init__.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,13 @@ def create_new_diff(
10911091
packages
10921092
) = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id)
10931093

1094-
diff = self.create_diff_report(added_packages, removed_packages)
1094+
# Separate unchanged packages from added/removed for --strict-blocking support
1095+
unchanged_packages = {
1096+
pkg_id: pkg for pkg_id, pkg in packages.items()
1097+
if pkg_id not in added_packages and pkg_id not in removed_packages
1098+
}
1099+
1100+
diff = self.create_diff_report(added_packages, removed_packages, unchanged_packages)
10951101
diff.packages = packages
10961102

10971103
base_socket = "https://socket.dev/dashboard/org"
@@ -1114,6 +1120,7 @@ def create_diff_report(
11141120
self,
11151121
added_packages: Dict[str, Package],
11161122
removed_packages: Dict[str, Package],
1123+
unchanged_packages: Optional[Dict[str, Package]] = None,
11171124
direct_only: bool = True
11181125
) -> Diff:
11191126
"""
@@ -1123,10 +1130,12 @@ def create_diff_report(
11231130
1. Records new/removed packages (direct only by default)
11241131
2. Collects alerts from both sets of packages
11251132
3. Determines new capabilities introduced
1133+
4. Optionally collects alerts from unchanged packages for --strict-blocking
11261134
11271135
Args:
11281136
added_packages: Dict of packages added in new scan
11291137
removed_packages: Dict of packages removed in new scan
1138+
unchanged_packages: Dict of packages that didn't change (for --strict-blocking)
11301139
direct_only: If True, only direct dependencies are included in new/removed lists
11311140
(but alerts are still processed for all packages)
11321141
@@ -1137,6 +1146,7 @@ def create_diff_report(
11371146

11381147
alerts_in_added_packages: Dict[str, List[Issue]] = {}
11391148
alerts_in_removed_packages: Dict[str, List[Issue]] = {}
1149+
alerts_in_unchanged_packages: Dict[str, List[Issue]] = {}
11401150

11411151
seen_new_packages = set()
11421152
seen_removed_packages = set()
@@ -1169,11 +1179,34 @@ def create_diff_report(
11691179
packages=removed_packages
11701180
)
11711181

1182+
# Process unchanged packages for --strict-blocking support
1183+
if unchanged_packages:
1184+
for package_id, package in unchanged_packages.items():
1185+
# Skip packages that are in added or removed (they're already processed)
1186+
if package_id in added_packages or package_id in removed_packages:
1187+
continue
1188+
1189+
self.add_package_alerts_to_collection(
1190+
package=package,
1191+
alerts_collection=alerts_in_unchanged_packages,
1192+
packages=unchanged_packages
1193+
)
1194+
11721195
diff.new_alerts = Core.get_new_alerts(
11731196
alerts_in_added_packages,
11741197
alerts_in_removed_packages
11751198
)
11761199

1200+
# Get unchanged alerts (for --strict-blocking mode)
1201+
diff.unchanged_alerts = Core.get_unchanged_alerts(
1202+
alerts_in_unchanged_packages
1203+
)
1204+
1205+
# Get removed alerts (for completeness)
1206+
diff.removed_alerts = Core.get_removed_alerts(
1207+
alerts_in_removed_packages
1208+
)
1209+
11771210
diff.new_capabilities = Core.get_capabilities_for_added_packages(added_packages)
11781211

11791212
Core.add_purl_capabilities(diff)
@@ -1433,3 +1466,62 @@ def get_new_alerts(
14331466
consolidated_alerts.add(alert_str)
14341467

14351468
return alerts
1469+
1470+
@staticmethod
1471+
def get_unchanged_alerts(
1472+
unchanged_package_alerts: Dict[str, List[Issue]]
1473+
) -> List[Issue]:
1474+
"""
1475+
Extract all alerts from unchanged packages that are errors or warnings.
1476+
1477+
This is used for --strict-blocking mode to identify existing violations
1478+
that should cause builds to fail.
1479+
1480+
Args:
1481+
unchanged_package_alerts: Dictionary of alerts from packages that didn't change
1482+
1483+
Returns:
1484+
List of all error/warning alerts from unchanged packages
1485+
"""
1486+
alerts: List[Issue] = []
1487+
consolidated_alerts = set()
1488+
1489+
for alert_key in unchanged_package_alerts:
1490+
for alert in unchanged_package_alerts[alert_key]:
1491+
# Consolidate by package and alert type
1492+
alert_str = f"{alert.purl},{alert.type}"
1493+
1494+
# Only include error or warning alerts
1495+
if (alert.error or alert.warn) and alert_str not in consolidated_alerts:
1496+
alerts.append(alert)
1497+
consolidated_alerts.add(alert_str)
1498+
1499+
return alerts
1500+
1501+
@staticmethod
1502+
def get_removed_alerts(
1503+
removed_package_alerts: Dict[str, List[Issue]]
1504+
) -> List[Issue]:
1505+
"""
1506+
Extract all alerts from removed packages.
1507+
1508+
This is mainly for informational purposes - to show alerts that were removed.
1509+
1510+
Args:
1511+
removed_package_alerts: Dictionary of alerts from packages that were removed
1512+
1513+
Returns:
1514+
List of all alerts from removed packages
1515+
"""
1516+
alerts: List[Issue] = []
1517+
consolidated_alerts = set()
1518+
1519+
for alert_key in removed_package_alerts:
1520+
for alert in removed_package_alerts[alert_key]:
1521+
alert_str = f"{alert.purl},{alert.type}"
1522+
1523+
if alert_str not in consolidated_alerts:
1524+
alerts.append(alert)
1525+
consolidated_alerts.add(alert_str)
1526+
1527+
return alerts

socketsecurity/core/classes.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,8 @@ class Diff:
474474
packages: dict[str, Package]
475475
new_capabilities: Dict[str, List[str]]
476476
new_alerts: list[Issue]
477+
unchanged_alerts: list[Issue]
478+
removed_alerts: list[Issue]
477479
id: str
478480
sbom: str
479481
report_url: str
@@ -490,6 +492,10 @@ def __init__(self, **kwargs):
490492
self.removed_packages = []
491493
if not hasattr(self, "new_alerts"):
492494
self.new_alerts = []
495+
if not hasattr(self, "unchanged_alerts"):
496+
self.unchanged_alerts = []
497+
if not hasattr(self, "removed_alerts"):
498+
self.removed_alerts = []
493499
if not hasattr(self, "new_capabilities"):
494500
self.new_capabilities = {}
495501

@@ -508,6 +514,8 @@ def to_dict(self) -> dict:
508514
"new_capabilities": self.new_capabilities,
509515
"removed_packages": [p.to_dict() for p in self.removed_packages],
510516
"new_alerts": [alert.__dict__ for alert in self.new_alerts],
517+
"unchanged_alerts": [alert.__dict__ for alert in self.unchanged_alerts] if hasattr(self, "unchanged_alerts") else [],
518+
"removed_alerts": [alert.__dict__ for alert in self.removed_alerts] if hasattr(self, "removed_alerts") else [],
511519
"id": self.id,
512520
"sbom": self.sbom if hasattr(self, "sbom") else [],
513521
"packages": {k: v.to_dict() for k, v in self.packages.items()} if hasattr(self, "packages") else {},

0 commit comments

Comments
 (0)