1515def _get_github_pr_result_limit () -> int :
1616 """Get the result limit for GitHub PR notifications."""
1717 try :
18- notifications_yaml = Path (__file__ ).parent .parent .parent / 'notifications.yaml'
18+ notifications_yaml = Path (__file__ ).parent .parent .parent . parent / 'notifications.yaml'
1919 with open (notifications_yaml , 'r' ) as f :
2020 config = yaml .safe_load (f )
2121 return config .get ('settings' , {}).get ('result_limits' , {}).get ('github_pr' , 100 )
@@ -30,124 +30,156 @@ def format_notifications(groups: Dict[str, List[Dict[str, Any]]], config=None) -
3030
3131 # Map subtypes to friendly display names
3232 subtype_names = {
33- 'sast-python' : 'SAST Python' ,
34- 'sast-javascript' : 'SAST JavaScript' ,
35- 'sast-golang' : 'SAST Go' ,
36- 'sast-java' : 'SAST Java' ,
37- 'sast-php' : 'SAST PHP' ,
38- 'sast-ruby' : 'SAST Ruby' ,
39- 'sast-csharp' : 'SAST C#' ,
40- 'sast-dotnet' : 'SAST .NET' ,
41- 'sast-c' : 'SAST C' ,
42- 'sast-cpp' : 'SAST C++' ,
43- 'sast-kotlin' : 'SAST Kotlin' ,
44- 'sast-scala' : 'SAST Scala' ,
45- 'sast-swift' : 'SAST Swift' ,
46- 'sast-rust' : 'SAST Rust' ,
33+ 'sast-python' : 'Socket SAST Python' ,
34+ 'sast-javascript' : 'Socket SAST JavaScript' ,
35+ 'sast-golang' : 'Socket SAST Go' ,
36+ 'sast-java' : 'Socket SAST Java' ,
37+ 'sast-php' : 'Socket SAST PHP' ,
38+ 'sast-ruby' : 'Socket SAST Ruby' ,
39+ 'sast-csharp' : 'Socket SAST C#' ,
40+ 'sast-dotnet' : 'Socket SAST .NET' ,
41+ 'sast-c' : 'Socket SAST C' ,
42+ 'sast-cpp' : 'Socket SAST C++' ,
43+ 'sast-kotlin' : 'Socket SAST Kotlin' ,
44+ 'sast-scala' : 'Socket SAST Scala' ,
45+ 'sast-swift' : 'Socket SAST Swift' ,
46+ 'sast-rust' : 'Socket SAST Rust' ,
4747 }
4848
4949 severity_order = {'critical' : 0 , 'high' : 1 , 'medium' : 2 , 'low' : 3 }
50+ severity_emoji = {
51+ 'critical' : '🔴' ,
52+ 'high' : '🟠' ,
53+ 'medium' : '🟡' ,
54+ 'low' : '⚪'
55+ }
5056
5157 for subtype , items in groups .items ():
52- rows = []
58+ # Group findings by file path, then by rule within each file
59+ file_groups = {} # {file_path: {rule_id: [(severity, start, end, code_snippet), ...]}}
60+ severity_counts = {'critical' : 0 , 'high' : 0 , 'medium' : 0 , 'low' : 0 }
61+
5362 for item in items :
5463 c = item ['component' ]
5564 a = item ['alert' ]
5665 props = a .get ('props' , {}) or {}
5766 full_path = props .get ('filePath' , a .get ('location' , {}).get ('path' )) or '-'
67+ rule_id = props .get ('ruleId' , a .get ('title' , '' ))
68+ severity = a .get ('severity' , '' ).lower ()
69+ start_line = props .get ('startLine' , '' )
70+ end_line = props .get ('endLine' , '' )
71+ code_snippet = props .get ('codeSnippet' , '' ) or ''
5872
59- try :
60- file_name = Path (full_path ).name
61- except Exception :
62- file_name = full_path
73+ # Count by severity
74+ if severity in severity_counts :
75+ severity_counts [severity ] += 1
6376
64- # Format code snippets with <pre> tags and <br> for line breaks
65- code_snippet = props .get ('codeSnippet' , '' ) or ''
66- if code_snippet :
67- # Use <pre> tags for better code formatting as requested
68- code_formatted = code_snippet .replace ('\n ' , '<br>' )
69- if len (code_formatted ) > 200 :
70- code_formatted = code_formatted [:200 ] + '...'
71- code_snippet = f"<pre>{ code_formatted } </pre>"
72- else :
73- code_snippet = '-'
77+ # Group by file path
78+ if full_path not in file_groups :
79+ file_groups [full_path ] = {}
7480
75- severity = a .get ('severity' , '' ).lower ()
76- rows .append ((
77- severity_order .get (severity , 4 ),
78- [
79- f"**{ props .get ('ruleId' , a .get ('title' , '' ))} **" ,
80- f"*{ a .get ('severity' , '' )} *" ,
81- f"`{ file_name } `" ,
82- f"`{ full_path } `" ,
83- f"Lines { props .get ('startLine' ,'' )} -{ props .get ('endLine' ,'' )} " ,
84- code_snippet
85- ]
86- ))
87-
88- # Sort by severity and extract rows
89- rows .sort (key = lambda x : x [0 ])
90- rows = [row [1 ] for row in rows ]
81+ # Group by rule within file
82+ if rule_id not in file_groups [full_path ]:
83+ file_groups [full_path ][rule_id ] = []
84+
85+ file_groups [full_path ][rule_id ].append ({
86+ 'severity' : severity ,
87+ 'start_line' : start_line ,
88+ 'end_line' : end_line ,
89+ 'code_snippet' : code_snippet
90+ })
9191
92- # Apply truncation
93- # result_limit = _get_github_pr_result_limit()
94- total_results = len (rows )
95- was_truncated = False
96- #
97- # if total_results > result_limit:
98- # logger.info(f"Truncating GitHub PR OpenGrep results from {total_results} to {result_limit} (prioritized by severity)")
99- # rows = rows[:result_limit]
100- # was_truncated = True
92+ # Build content in requested format
93+ display_name = subtype_names .get (subtype , f"Socket { subtype .upper ()} " )
10194
102- # Create markdown table for this subtype
103- display_name = subtype_names .get (subtype , subtype .upper ())
104- if not rows :
105- content = f"No { display_name } issues found."
95+ if not file_groups :
96+ content = f"✅ No issues found."
10697 else :
107- headers = ['Rule' , 'Severity' , 'File' , 'Path' , 'Lines' , 'Code' ]
108- header_row = '| ' + ' | ' .join (headers ) + ' |'
109- separator_row = '| ' + ' | ' .join (['---' ] * len (headers )) + ' |'
110- content_rows = []
111- for row in rows :
112- content_rows .append ('| ' + ' | ' .join (str (cell ) for cell in row ) + ' |' )
98+ content_lines = []
11399
114- content = '\n ' .join ([header_row , separator_row ] + content_rows )
100+ # Add summary
101+ content_lines .append ("### Summary" )
102+ content_lines .append (f"{ severity_emoji .get ('critical' , '🔴' )} Critical: { severity_counts ['critical' ]} | "
103+ f"{ severity_emoji .get ('high' , '🟠' )} High: { severity_counts ['high' ]} | "
104+ f"{ severity_emoji .get ('medium' , '🟡' )} Medium: { severity_counts ['medium' ]} | "
105+ f"{ severity_emoji .get ('low' , '⚪' )} Low: { severity_counts ['low' ]} " )
106+ content_lines .append ("" )
107+ content_lines .append ("### Details" )
108+ content_lines .append ("" )
115109
116- # Add truncation notice if needed
117- # if was_truncated:
118- # content += f"\n\n⚠️ **Results truncated to {result_limit} highest severity findings** (total: {total_results}). See full scan URL for complete results."
110+ # Sort files by highest severity finding in each file
111+ file_severity_list = []
112+ for file_path in file_groups .keys ():
113+ # Find the highest severity (lowest number) in this file
114+ min_severity = 999
115+ for rule_id , locations in file_groups [file_path ].items ():
116+ for loc in locations :
117+ sev = severity_order .get (loc ['severity' ], 4 )
118+ if sev < min_severity :
119+ min_severity = sev
120+ file_severity_list .append ((min_severity , file_path ))
121+
122+ # Sort by severity first, then by file path
123+ file_severity_list .sort (key = lambda x : (x [0 ], x [1 ]))
124+
125+ for _ , file_path in file_severity_list :
126+ try :
127+ file_name = Path (file_path ).name
128+ except Exception :
129+ file_name = file_path
130+
131+ # File header
132+ content_lines .append (f"#### `{ file_path } `" )
133+ content_lines .append ("" )
134+
135+ # Sort rules by severity within file
136+ rules_in_file = []
137+ for rule_id , locations in file_groups [file_path ].items ():
138+ # Get highest severity for this rule
139+ min_severity = min (severity_order .get (loc ['severity' ], 4 ) for loc in locations )
140+ rules_in_file .append ((min_severity , rule_id , locations ))
141+
142+ rules_in_file .sort (key = lambda x : x [0 ])
143+
144+ # Output each rule with its locations
145+ for _ , rule_id , locations in rules_in_file :
146+ # Get severity from first location (they should all be same rule)
147+ rule_severity = locations [0 ]['severity' ]
148+ emoji = severity_emoji .get (rule_severity , '⚪' )
149+
150+ content_lines .append (f"**{ rule_id } ** " )
151+ content_lines .append (f"{ emoji } *{ rule_severity .upper ()} *" )
152+ content_lines .append ("" )
153+
154+ # Output each location with code snippet
155+ for loc in locations :
156+ content_lines .append (f"**Lines { loc ['start_line' ]} :{ loc ['end_line' ]} **" )
157+ if loc ['code_snippet' ]:
158+ # Format code snippet in code block
159+ content_lines .append ("```" )
160+ content_lines .append (loc ['code_snippet' ])
161+ content_lines .append ("```" )
162+ content_lines .append ("" )
163+
164+ content = '\n ' .join (content_lines )
119165
120- # Build title with repo/branch/commit info from config
121- title_parts = ["Socket Security Results" ]
166+ # Build title
167+ title_parts = [display_name ]
122168 if config :
123169 if config .repo :
124170 title_parts .append (config .repo )
125171 if config .branch :
126172 title_parts .append (config .branch )
127173 if config .commit_hash :
128- title_parts .append (config .commit_hash )
174+ title_parts .append (config .commit_hash [: 8 ]) # Short hash
129175
130176 title = " - " .join (title_parts )
131177
132- # Count total findings for summary
133- total_findings = total_results if not was_truncated else total_results
134-
135- # Add summary section with scanner findings
136- summary_content = f"""## Summary
137-
138- | Scanner | Findings |
139- |---------|----------|
140- | { display_name } | { total_findings } |
141-
142- ## Details
143-
144- { content } """
145-
146178 # Wrap content with HTML comment markers for section updates
147179 wrapped_content = f"""<!-- { subtype } start -->
148180# { title }
149181
150- { summary_content }
182+ { content }
151183<!-- { subtype } end -->"""
152184
153185 tables .append ({
0 commit comments