Skip to content

Commit 4020285

Browse files
committed
Fix gitlab security report schema validation errors
Signed-off-by: lelia <2418071+lelia@users.noreply.github.com>
1 parent ced11ae commit 4020285

File tree

2 files changed

+168
-8
lines changed

2 files changed

+168
-8
lines changed

socketsecurity/core/messages.py

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
import re
55
import uuid
6-
from datetime import datetime
6+
from datetime import datetime, timezone
77
from pathlib import Path
88
from mdutils import MdUtils
99
from prettytable import PrettyTable
@@ -593,6 +593,20 @@ def create_security_comment_json(diff: Diff) -> dict:
593593
output["new_alerts"].append(json.loads(str(alert)))
594594
return output
595595

596+
@staticmethod
597+
def _pkg_type_to_package_manager(pkg_type: str) -> str:
598+
"""Map Socket pkg_type to GitLab package_manager name for dependency_files."""
599+
mapping = {
600+
"npm": "npm",
601+
"pypi": "pip",
602+
"go": "go",
603+
"maven": "maven",
604+
"gem": "bundler",
605+
"nuget": "nuget",
606+
"cargo": "cargo",
607+
}
608+
return mapping.get(pkg_type, pkg_type or "unknown")
609+
596610
@staticmethod
597611
def map_socket_severity_to_gitlab(severity: str) -> str:
598612
"""
@@ -743,14 +757,16 @@ def create_security_comment_gitlab(diff: Diff) -> dict:
743757
}
744758
},
745759
"type": "dependency_scanning",
746-
"start_time": datetime.utcnow().isoformat() + "Z",
747-
"end_time": datetime.utcnow().isoformat() + "Z",
760+
"start_time": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S"),
761+
"end_time": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S"),
748762
"status": "success"
749763
},
750-
"vulnerabilities": []
764+
"vulnerabilities": [],
765+
"dependency_files": []
751766
}
752767

753-
# Process each alert
768+
dep_files_map: dict = {}
769+
754770
for alert in diff.new_alerts:
755771
vulnerability = {
756772
"id": Messages.generate_uuid_from_alert_gitlab(alert),
@@ -764,12 +780,29 @@ def create_security_comment_gitlab(diff: Diff) -> dict:
764780
"location": Messages.extract_location_gitlab(alert)
765781
}
766782

767-
# Add solution if available
768783
if hasattr(alert, 'suggestion') and alert.suggestion:
769784
vulnerability["solution"] = alert.suggestion
770785

771786
gitlab_report["vulnerabilities"].append(vulnerability)
772787

788+
file_path = vulnerability["location"]["file"]
789+
if file_path != "unknown":
790+
pkg_manager = Messages._pkg_type_to_package_manager(
791+
alert.pkg_type if hasattr(alert, 'pkg_type') else ""
792+
)
793+
if file_path not in dep_files_map:
794+
dep_files_map[file_path] = {
795+
"path": file_path,
796+
"package_manager": pkg_manager,
797+
"dependencies": []
798+
}
799+
dep_files_map[file_path]["dependencies"].append({
800+
"package": {"name": alert.pkg_name},
801+
"version": alert.pkg_version
802+
})
803+
804+
gitlab_report["dependency_files"] = list(dep_files_map.values())
805+
773806
return gitlab_report
774807

775808
@staticmethod

tests/unit/test_gitlab_format.py

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
import pytest
24
from socketsecurity.core.messages import Messages
35
from socketsecurity.core.classes import Diff, Issue
@@ -6,19 +8,23 @@
68
class TestGitLabFormat:
79
"""Test suite for GitLab Security Dashboard format generation"""
810

11+
# GitLab v15.0.0 schema: ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$
12+
GITLAB_TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$")
13+
914
def test_gitlab_report_structure(self):
10-
"""Test basic GitLab report structure is valid"""
15+
"""Test basic GitLab report structure matches v15.0.0 schema requirements"""
1116
diff = Diff()
1217
diff.new_alerts = []
1318
diff.id = "test-scan-id"
1419
diff.diff_url = "https://socket.dev/test"
1520

1621
report = Messages.create_security_comment_gitlab(diff)
1722

18-
# Verify required top-level fields
23+
# All four root-level keys required by v15.0.0 schema
1924
assert "version" in report
2025
assert "scan" in report
2126
assert "vulnerabilities" in report
27+
assert "dependency_files" in report
2228

2329
# Verify scan structure
2430
assert report["scan"]["type"] == "dependency_scanning"
@@ -28,6 +34,12 @@ def test_gitlab_report_structure(self):
2834
assert report["scan"]["scanner"]["id"] == "socket-cli"
2935
assert report["scan"]["status"] == "success"
3036

37+
# Timestamps must match GitLab pattern (no microseconds, no trailing Z)
38+
assert self.GITLAB_TIMESTAMP_RE.match(report["scan"]["start_time"]), \
39+
f"start_time '{report['scan']['start_time']}' doesn't match GitLab pattern"
40+
assert self.GITLAB_TIMESTAMP_RE.match(report["scan"]["end_time"]), \
41+
f"end_time '{report['scan']['end_time']}' doesn't match GitLab pattern"
42+
3143
def test_vulnerability_mapping(self):
3244
"""Test Socket Issue maps correctly to GitLab vulnerability"""
3345
diff = Diff()
@@ -391,3 +403,118 @@ def test_manifests_attribute_fallback(self):
391403
vuln = report["vulnerabilities"][0]
392404

393405
assert vuln["location"]["file"] == "requirements.txt"
406+
407+
def test_dependency_files_populated_from_alerts(self):
408+
"""Test dependency_files is built from alert locations per v15.0.0 schema"""
409+
diff = Diff()
410+
diff.id = "test-scan-id"
411+
diff.diff_url = "https://socket.dev/test"
412+
413+
diff.new_alerts = [
414+
Issue(
415+
pkg_name="pkg-a",
416+
pkg_version="1.0.0",
417+
type="malware",
418+
severity="high",
419+
title="Alert A",
420+
manifests="package.json",
421+
pkg_type="npm",
422+
key="key-a",
423+
purl="pkg:npm/pkg-a@1.0.0"
424+
),
425+
Issue(
426+
pkg_name="pkg-b",
427+
pkg_version="2.0.0",
428+
type="vulnerability",
429+
severity="medium",
430+
title="Alert B",
431+
manifests="requirements.txt",
432+
pkg_type="pypi",
433+
key="key-b",
434+
purl="pkg:pypi/pkg-b@2.0.0"
435+
),
436+
]
437+
438+
report = Messages.create_security_comment_gitlab(diff)
439+
440+
assert "dependency_files" in report
441+
dep_files = report["dependency_files"]
442+
assert len(dep_files) == 2
443+
444+
paths = {df["path"] for df in dep_files}
445+
assert "package.json" in paths
446+
assert "requirements.txt" in paths
447+
448+
for df in dep_files:
449+
assert "path" in df
450+
assert "package_manager" in df
451+
assert "dependencies" in df
452+
assert isinstance(df["dependencies"], list)
453+
assert len(df["dependencies"]) >= 1
454+
for dep in df["dependencies"]:
455+
assert "package" in dep
456+
assert "name" in dep["package"]
457+
assert "version" in dep
458+
459+
def test_dependency_files_groups_by_manifest(self):
460+
"""Test multiple alerts from the same manifest are grouped into one dependency_file"""
461+
diff = Diff()
462+
diff.id = "test-scan-id"
463+
diff.diff_url = "https://socket.dev/test"
464+
465+
diff.new_alerts = [
466+
Issue(
467+
pkg_name="pkg-a", pkg_version="1.0.0", type="malware", severity="high",
468+
title="A", manifests="package.json", pkg_type="npm", key="k1", purl="pkg:npm/pkg-a@1.0.0"
469+
),
470+
Issue(
471+
pkg_name="pkg-b", pkg_version="2.0.0", type="vulnerability", severity="low",
472+
title="B", manifests="package.json", pkg_type="npm", key="k2", purl="pkg:npm/pkg-b@2.0.0"
473+
),
474+
]
475+
476+
report = Messages.create_security_comment_gitlab(diff)
477+
dep_files = report["dependency_files"]
478+
479+
assert len(dep_files) == 1
480+
assert dep_files[0]["path"] == "package.json"
481+
assert dep_files[0]["package_manager"] == "npm"
482+
assert len(dep_files[0]["dependencies"]) == 2
483+
484+
def test_dependency_files_empty_when_no_alerts(self):
485+
"""Test dependency_files is an empty array when there are no alerts"""
486+
diff = Diff()
487+
diff.id = "test-scan-id"
488+
diff.diff_url = "https://socket.dev/test"
489+
diff.new_alerts = []
490+
491+
report = Messages.create_security_comment_gitlab(diff)
492+
assert report["dependency_files"] == []
493+
494+
def test_dependency_files_skips_unknown_manifest(self):
495+
"""Test alerts with unknown manifest don't produce dependency_file entries"""
496+
diff = Diff()
497+
diff.id = "test-scan-id"
498+
diff.diff_url = "https://socket.dev/test"
499+
500+
diff.new_alerts = [
501+
Issue(
502+
pkg_name="pkg-a", pkg_version="1.0.0", type="malware", severity="high",
503+
title="A", pkg_type="npm", key="k1", purl="pkg:npm/pkg-a@1.0.0"
504+
),
505+
]
506+
507+
report = Messages.create_security_comment_gitlab(diff)
508+
assert report["dependency_files"] == []
509+
510+
def test_pkg_type_to_package_manager_mapping(self):
511+
"""Test package manager mapping covers common ecosystems"""
512+
assert Messages._pkg_type_to_package_manager("npm") == "npm"
513+
assert Messages._pkg_type_to_package_manager("pypi") == "pip"
514+
assert Messages._pkg_type_to_package_manager("go") == "go"
515+
assert Messages._pkg_type_to_package_manager("maven") == "maven"
516+
assert Messages._pkg_type_to_package_manager("gem") == "bundler"
517+
assert Messages._pkg_type_to_package_manager("nuget") == "nuget"
518+
assert Messages._pkg_type_to_package_manager("cargo") == "cargo"
519+
assert Messages._pkg_type_to_package_manager("") == "unknown"
520+
assert Messages._pkg_type_to_package_manager("swift") == "swift"

0 commit comments

Comments
 (0)