1414listed by the per-stream LABEL, which is index-independent and immediately consistent, so
1515near-simultaneous runs cannot create duplicates.
1616
17- Closed issues are never touched: a reappearing vulnerability always gets a fresh issue.
17+ Closed issues are never re-opened. A vulnerability whose key matches a closed
18+ issue carrying a "won't fix" label (any case, ``_`` or space) is suppressed and
19+ not re-created. Other reappearing vulnerabilities get a fresh issue.
1820"""
1921
2022from __future__ import annotations
3638# Severity labels are mutually exclusive, so they are reset (not just added) on update.
3739SEVERITY_LABELS = {"CRITICAL" , "HIGH" , "MODERATE" , "MEDIUM" , "LOW" }
3840
41+ # Labels marking a closed issue as "won't fix"; matched case-insensitively, with
42+ # ``_`` and space treated as equivalent. A vuln whose key matches a closed issue
43+ # carrying any of these labels is not re-opened on subsequent scans.
44+ WONT_FIX_LABEL = "WONT FIX" # normalized form (uppercase, underscore -> space)
45+
46+
47+ def _is_wont_fix_label (label_name : str ) -> bool :
48+ return label_name .upper ().replace ("_" , " " ).strip () == WONT_FIX_LABEL
49+
3950
4051def eprint (* args : Any ) -> None :
4152 print (* args , file = sys .stderr )
@@ -205,6 +216,20 @@ def list_open_issues(self, stream: str, limit: int = 1000) -> List[Dict[str, Any
205216 eprint (f"WARNING: open-issue list hit the limit of { limit } ; results may be truncated" )
206217 return issues
207218
219+ def list_closed_issues (self , stream : str , limit : int = 1000 ) -> List [Dict [str , Any ]]:
220+ """Closed issues for the stream, used to detect won't-fix suppressions."""
221+ out = self ._run ([
222+ "issue" , "list" ,
223+ "--state" , "closed" ,
224+ "--label" , stream ,
225+ "--limit" , str (limit ),
226+ "--json" , "number,title,body,labels" ,
227+ ])
228+ issues = json .loads (out ) if out .strip () else []
229+ if len (issues ) >= limit :
230+ eprint (f"WARNING: closed-issue list hit the limit of { limit } ; results may be truncated" )
231+ return issues
232+
208233 def create_issue (self , title : str , body : str ) -> Optional [int ]:
209234 out = self ._run (
210235 ["issue" , "create" , "--title" , title , "--body-file" , "-" ],
@@ -288,7 +313,20 @@ def reconcile(scan: Dict[str, Any], stream: str, action_url: str, gh: Gh) -> int
288313 recognizable .add (issue ["number" ])
289314
290315 matched : set = set ()
291- created = updated = closed = 0
316+ created = updated = closed = skipped = 0
317+
318+ # Build the set of suppressed keys from closed issues marked won't fix.
319+ suppressed : set = set ()
320+ for issue in gh .list_closed_issues (stream ):
321+ if not any (_is_wont_fix_label (lbl .get ("name" , "" )) for lbl in issue .get ("labels" , [])):
322+ continue
323+ mk = extract_key (issue .get ("body" ))
324+ if mk :
325+ suppressed .add (mk )
326+ continue
327+ lk = legacy_key_from_title (issue .get ("title" , "" ))
328+ if lk :
329+ suppressed .add (lk )
292330
293331 # Create / update.
294332 for pkey , vuln in desired .items ():
@@ -300,6 +338,9 @@ def reconcile(scan: Dict[str, Any], stream: str, action_url: str, gh: Gh) -> int
300338 matched .add (issue ["number" ])
301339 updated += 1
302340 eprint (f"Updated #{ issue ['number' ]} : { render_title (stream , vuln )} " )
341+ elif pkey in suppressed or legacy_key_for_vuln (stream , vuln ) in suppressed :
342+ skipped += 1
343+ eprint (f"Skipped (won't fix): { render_title (stream , vuln )} " )
303344 else :
304345 number = gh .create_issue (render_title (stream , vuln ), body )
305346 if number is not None :
@@ -324,7 +365,7 @@ def reconcile(scan: Dict[str, Any], stream: str, action_url: str, gh: Gh) -> int
324365 else :
325366 eprint ("Scan incomplete (scan_complete=false): skipping close phase to avoid false closes" )
326367
327- eprint (f"Reconcile summary for { stream } : created={ created } updated={ updated } closed={ closed } " )
368+ eprint (f"Reconcile summary for { stream } : created={ created } updated={ updated } closed={ closed } skipped= { skipped } " )
328369 return 0
329370
330371
0 commit comments