Skip to content

Commit 82d300b

Browse files
authored
Moved to new Diff endpoint and fix with commenting logic (#88)
* Version bump * Cherry picked 73e1ce2 back in * Cherry picked missing commit * Removed unneeded full scann processing
1 parent 43a9c2e commit 82d300b

File tree

8 files changed

+175
-38
lines changed

8 files changed

+175
-38
lines changed

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.1.3"
9+
version = "2.1.9"
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,2 +1,2 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.1.3'
2+
__version__ = '2.1.9'

socketsecurity/core/__init__.py

Lines changed: 35 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,25 +133,40 @@ def create_sbom_output(self, diff: Diff) -> dict:
133133
@staticmethod
134134
def expand_brace_pattern(pattern: str) -> List[str]:
135135
"""
136-
Expands brace expressions (e.g., {a,b,c}) into separate patterns.
137-
"""
138-
brace_regex = re.compile(r"\{([^{}]+)\}")
139-
140-
# Expand all brace groups
141-
expanded_patterns = [pattern]
142-
while any("{" in p for p in expanded_patterns):
143-
new_patterns = []
144-
for pat in expanded_patterns:
145-
match = brace_regex.search(pat)
146-
if match:
147-
options = match.group(1).split(",") # Extract values inside {}
148-
prefix, suffix = pat[:match.start()], pat[match.end():]
149-
new_patterns.extend([prefix + opt + suffix for opt in options])
150-
else:
151-
new_patterns.append(pat)
152-
expanded_patterns = new_patterns
153-
154-
return expanded_patterns
136+
Recursively expands brace expressions (e.g., {a,b,c}) into separate patterns, supporting nested braces.
137+
"""
138+
def recursive_expand(pat: str) -> List[str]:
139+
stack = []
140+
for i, c in enumerate(pat):
141+
if c == '{':
142+
stack.append(i)
143+
elif c == '}' and stack:
144+
start = stack.pop()
145+
if not stack:
146+
# Found the outermost pair
147+
before = pat[:start]
148+
after = pat[i+1:]
149+
inner = pat[start+1:i]
150+
# Split on commas not inside nested braces
151+
options = []
152+
depth = 0
153+
last = 0
154+
for j, ch in enumerate(inner):
155+
if ch == '{':
156+
depth += 1
157+
elif ch == '}':
158+
depth -= 1
159+
elif ch == ',' and depth == 0:
160+
options.append(inner[last:j])
161+
last = j+1
162+
options.append(inner[last:])
163+
results = []
164+
for opt in options:
165+
expanded = before + opt + after
166+
results.extend(recursive_expand(expanded))
167+
return results
168+
return [pat]
169+
return recursive_expand(pattern)
155170

156171
@staticmethod
157172
def is_excluded(file_path: str, excluded_dirs: Set[str]) -> bool:
@@ -176,13 +191,7 @@ def find_files(self, path: str) -> List[str]:
176191
files: Set[str] = set()
177192

178193
# Get supported patterns from the API
179-
try:
180-
patterns = self.get_supported_patterns()
181-
except Exception as e:
182-
log.error(f"Error getting supported patterns from API: {e}")
183-
log.warning("Falling back to local patterns")
184-
from .utils import socket_globs as fallback_patterns
185-
patterns = fallback_patterns
194+
patterns = self.get_supported_patterns()
186195

187196
for ecosystem in patterns:
188197
if ecosystem in self.config.excluded_ecosystems:
@@ -642,7 +651,6 @@ def create_new_diff(
642651
try:
643652
new_scan_start = time.time()
644653
new_full_scan = self.create_full_scan(files_for_sending, params)
645-
new_full_scan.sbom_artifacts = self.get_sbom_data(new_full_scan.id)
646654
new_scan_end = time.time()
647655
log.info(f"Total time to create new full scan: {new_scan_end - new_scan_start:.2f}")
648656
except APIFailure as e:

socketsecurity/core/classes.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class AlertCounts(TypedDict):
9797
low: int
9898

9999
@dataclass(kw_only=True)
100-
class Package(SocketArtifactLink):
100+
class Package():
101101
"""
102102
Represents a package detected in a Socket Security scan.
103103
@@ -106,16 +106,23 @@ class Package(SocketArtifactLink):
106106
"""
107107

108108
# Common properties from both artifact types
109-
id: str
109+
type: str
110110
name: str
111111
version: str
112-
type: str
112+
release: str
113+
diffType: str
114+
id: str
115+
author: List[str] = field(default_factory=list)
113116
score: SocketScore
114117
alerts: List[SocketAlert]
115-
author: List[str] = field(default_factory=list)
116118
size: Optional[int] = None
117119
license: Optional[str] = None
118120
namespace: Optional[str] = None
121+
topLevelAncestors: Optional[List[str]] = None
122+
direct: Optional[bool] = False
123+
manifestFiles: Optional[List[SocketManifestReference]] = None
124+
dependencies: Optional[List[str]] = None
125+
artifact: Optional[SocketArtifactLink] = None
119126

120127
# Package-specific fields
121128
license_text: str = ""
@@ -203,7 +210,9 @@ def from_diff_artifact(cls, data: dict) -> "Package":
203210
manifestFiles=ref.get("manifestFiles", []),
204211
dependencies=ref.get("dependencies"),
205212
artifact=ref.get("artifact"),
206-
namespace=data.get('namespace', None)
213+
namespace=data.get('namespace', None),
214+
release=ref.get("release", None),
215+
diffType=ref.get("diffType", None),
207216
)
208217

209218
class Issue:
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import markdown
2+
from bs4 import BeautifulSoup, NavigableString, Tag
3+
import string
4+
5+
6+
class Helper:
7+
@staticmethod
8+
def parse_gfm_section(html_content):
9+
"""
10+
Parse a GitHub-Flavored Markdown section containing a table and surrounding content.
11+
Returns a dict with "before_html", "columns", "rows_html", and "after_html".
12+
"""
13+
html = markdown.markdown(html_content, extensions=['extra'])
14+
soup = BeautifulSoup(html, "html.parser")
15+
16+
table = soup.find('table')
17+
if not table:
18+
# If no table, treat entire content as before_html
19+
return {"before_html": html, "columns": [], "rows_html": [], "after_html": ''}
20+
21+
# Collect HTML before the table
22+
before_parts = [str(elem) for elem in table.find_previous_siblings()]
23+
before_html = ''.join(reversed(before_parts))
24+
25+
# Collect HTML after the table
26+
after_parts = [str(elem) for elem in table.find_next_siblings()]
27+
after_html = ''.join(after_parts)
28+
29+
# Extract table headers
30+
headers = [th.get_text(strip=True) for th in table.find_all('th')]
31+
32+
# Extract table rows (skip header)
33+
rows_html = []
34+
for tr in table.find_all('tr')[1:]:
35+
cells = [str(td) for td in tr.find_all('td')]
36+
rows_html.append(cells)
37+
38+
return {
39+
"before_html": before_html,
40+
"columns": headers,
41+
"rows_html": rows_html,
42+
"after_html": after_html
43+
}
44+
45+
@staticmethod
46+
def parse_cell(html_td):
47+
"""Convert a table cell HTML into plain text or a dict for links/images."""
48+
soup = BeautifulSoup(html_td, "html.parser")
49+
a = soup.find('a')
50+
if a:
51+
cell = {"url": a.get('href', '')}
52+
img = a.find('img')
53+
if img:
54+
cell.update({
55+
"img_src": img.get('src', ''),
56+
"title": img.get('title', ''),
57+
"link_text": a.get_text(strip=True)
58+
})
59+
else:
60+
cell["link_text"] = a.get_text(strip=True)
61+
return cell
62+
return soup.get_text(strip=True)
63+
64+
@staticmethod
65+
def parse_html_parts(html_fragment):
66+
"""
67+
Convert an HTML fragment into a list of parts.
68+
Each part is either:
69+
- {"text": "..."}
70+
- {"link": "url", "text": "..."}
71+
- {"img_src": "url", "alt": "...", "title": "..."}
72+
"""
73+
soup = BeautifulSoup(html_fragment, 'html.parser')
74+
parts = []
75+
76+
def handle_element(elem):
77+
if isinstance(elem, NavigableString):
78+
text = str(elem).strip()
79+
if text and not all(ch in string.punctuation for ch in text):
80+
parts.append({"text": text})
81+
elif isinstance(elem, Tag):
82+
if elem.name == 'a':
83+
href = elem.get('href', '')
84+
txt = elem.get_text(strip=True)
85+
parts.append({"link": href, "text": txt})
86+
elif elem.name == 'img':
87+
parts.append({
88+
"img_src": elem.get('src', ''),
89+
"alt": elem.get('alt', ''),
90+
"title": elem.get('title', '')
91+
})
92+
else:
93+
# Recurse into children for nested tags
94+
for child in elem.children:
95+
handle_element(child)
96+
97+
for element in soup.contents:
98+
handle_element(element)
99+
100+
return parts
101+
102+
@staticmethod
103+
def section_to_json(section_result):
104+
"""
105+
Convert a parsed section into structured JSON.
106+
Returns {"before": [...], "table": [...], "after": [...]}.
107+
"""
108+
# Build JSON rows for the table
109+
table_rows = []
110+
cols = section_result.get('columns', [])
111+
for row_html in section_result.get('rows_html', []):
112+
cells = [Helper.parse_cell(cell_html) for cell_html in row_html]
113+
table_rows.append(dict(zip(cols, cells)))
114+
115+
return {
116+
"before": Helper.parse_html_parts(section_result.get('before_html', '')),
117+
"table": table_rows,
118+
"after": Helper.parse_html_parts(section_result.get('after_html', ''))
119+
}

socketsecurity/core/messages.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ def create_security_comment_json(diff: Diff) -> dict:
292292
output = {
293293
"scan_failed": scan_failed,
294294
"new_alerts": [],
295-
"full_scan_id": diff.id
295+
"full_scan_id": diff.id,
296+
"diff_url": diff.diff_url
296297
}
297298
for alert in diff.new_alerts:
298299
alert: Issue

socketsecurity/output.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[st
6666

6767
console_security_comment = Messages.create_console_security_alert_table(diff_report)
6868
self.logger.info("Security issues detected by Socket Security:")
69-
self.logger.info(console_security_comment)
69+
self.logger.info(f"Diff Url: {diff_report.diff_url}")
70+
self.logger.info(f"\n{console_security_comment}")
7071

7172
def output_console_json(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None:
7273
"""Outputs JSON formatted results"""

socketsecurity/socketcli.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,14 @@ def main_code():
235235
log.debug("Updated security comment with no new alerts")
236236

237237
# FIXME: diff.new_packages is never populated, neither is removed_packages
238-
if (len(diff.new_packages) == 0 and len(diff.removed_packages) == 0) or config.disable_overview:
238+
if (len(diff.new_packages) == 0) or config.disable_overview:
239239
if not update_old_overview_comment:
240240
new_overview_comment = False
241241
log.debug("No new/removed packages or Dependency Overview comment disabled")
242242
else:
243243
log.debug("Updated overview comment with no dependencies")
244244

245245
log.debug(f"Adding comments for {config.scm}")
246-
247246
scm.add_socket_comments(
248247
security_comment,
249248
overview_comment,

0 commit comments

Comments
 (0)