Skip to content

Commit 651b33c

Browse files
authored
Merge pull request #261 from GeekMasher/codescanning-analyses
Code Scanning Analyses Updates
2 parents 51288b9 + b298868 commit 651b33c

File tree

4 files changed

+334
-25
lines changed

4 files changed

+334
-25
lines changed

examples/codescanning.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
print("Code Scanning is not enabled :(")
1919
exit()
2020

21-
# Get list of the delta alerts in a PR
2221
if GitHub.repository.isInPullRequest():
23-
alerts = cs.getAlertsInPR("refs/heads/main")
22+
# Get list of the delta alerts in a PR
23+
print(f"Alerts from PR :: {GitHub.repository.getPullRequestNumber()}")
24+
alerts = cs.getAlertsInPR(GitHub.repository.reference or "")
2425

25-
# Get all alerts
2626
else:
27+
# Get all alerts
28+
print("Alerts from default Branch")
2729
alerts = cs.getAlerts("open")
2830

2931
print(f"Alert Count :: {len(alerts)}")

src/ghastoolkit/octokit/codescanning.py

Lines changed: 202 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import json
55
import time
66
import logging
7-
from typing import Any, List, Optional
7+
from typing import Any, List, Optional, Union
88
from ghastoolkit.errors import GHASToolkitError, GHASToolkitTypeError
99
from ghastoolkit.octokit.github import GitHub, Repository
1010
from ghastoolkit.octokit.octokit import OctoItem, RestRequest, loadOctoItem
@@ -69,14 +69,149 @@ def __str__(self) -> str:
6969
return f"CodeAlert({self.number}, '{self.state}', '{self.tool_name}', '{self.rule_id}')"
7070

7171

72+
@dataclass
73+
class CodeScanningTool(OctoItem):
74+
"""Code Scanning Tool.
75+
76+
https://docs.github.com/rest/code-scanning/code-scanning#list-code-scanning-analyses-for-a-repository
77+
"""
78+
79+
name: str
80+
"""Tool Name"""
81+
guid: Optional[str] = None
82+
"""Tool GUID"""
83+
version: Optional[str] = None
84+
"""Tool Version"""
85+
86+
def __str__(self) -> str:
87+
"""To String."""
88+
if self.version:
89+
return f"CodeScanningTool({self.name}, '{self.version}')"
90+
else:
91+
return f"CodeScanningTool({self.name})"
92+
93+
def __repr__(self) -> str:
94+
return self.__str__()
95+
96+
97+
@dataclass
98+
class CodeScanningAnalysisEnvironment(OctoItem):
99+
"""Code Scanning Analysis Environment.
100+
101+
https://docs.github.com/rest/code-scanning/code-scanning
102+
"""
103+
104+
language: Optional[str] = None
105+
"""Language"""
106+
107+
build_mode: Optional[str] = None
108+
"""CodeQL Build Mode"""
109+
110+
def __str__(self) -> str:
111+
"""To String."""
112+
if self.language:
113+
return f"CodeScanningAnalysisEnvironment({self.language})"
114+
return "CodeScanningAnalysisEnvironment()"
115+
116+
117+
@dataclass
118+
class CodeScanningAnalysis(OctoItem):
119+
"""Code Scanning Analysis.
120+
121+
https://docs.github.com/rest/code-scanning/code-scanning#list-code-scanning-analyses-for-a-repository
122+
"""
123+
124+
id: int
125+
"""Unique Identifier"""
126+
ref: str
127+
"""Reference (branch, tag, etc)"""
128+
commit_sha: str
129+
"""Commit SHA"""
130+
analysis_key: str
131+
"""Analysis Key"""
132+
environment: CodeScanningAnalysisEnvironment
133+
"""Environment"""
134+
error: str
135+
"""Error"""
136+
created_at: str
137+
"""Created At"""
138+
results_count: int
139+
"""Results Count"""
140+
rules_count: int
141+
"""Rules Count"""
142+
url: str
143+
"""URL"""
144+
sarif_id: str
145+
"""SARIF ID"""
146+
tool: CodeScanningTool
147+
"""Tool Information"""
148+
deletable: bool
149+
"""Deletable"""
150+
warning: str
151+
"""Warning"""
152+
153+
category: Optional[str] = None
154+
"""Category"""
155+
156+
def __post_init__(self) -> None:
157+
if isinstance(self.environment, str):
158+
# Load the environment as JSON
159+
self.environment = loadOctoItem(
160+
CodeScanningAnalysisEnvironment, json.loads(self.environment)
161+
)
162+
if isinstance(self.tool, dict):
163+
# Load the tool as JSON
164+
self.tool = loadOctoItem(CodeScanningTool, self.tool)
165+
166+
@property
167+
def language(self) -> Optional[str]:
168+
"""Language from the Environment."""
169+
return self.environment.language
170+
171+
@property
172+
def build_mode(self) -> Optional[str]:
173+
"""Build Mode from the Environment."""
174+
return self.environment.build_mode
175+
176+
def __str__(self) -> str:
177+
"""To String."""
178+
return f"CodeScanningAnalysis({self.id}, '{self.ref}', '{self.tool.name}')"
179+
180+
181+
@dataclass
182+
class CodeScanningConfiguration(OctoItem):
183+
"""Code Scanning Configuration for Default Setup.
184+
185+
https://docs.github.com/rest/code-scanning/code-scanning#get-a-code-scanning-default-setup-configuration--parameters
186+
"""
187+
188+
state: str
189+
"""State of the Configuration"""
190+
query_suite: str
191+
"""Query Suite"""
192+
languages: list[str]
193+
"""Languages"""
194+
updated_at: str
195+
"""Updated At"""
196+
schedule: str
197+
"""Scheduled (weekly)"""
198+
199+
def __str__(self) -> str:
200+
"""To String."""
201+
return f"CodeScanningConfiguration('{self.state}', '{self.query_suite}', '{self.languages}')"
202+
203+
def __repr__(self) -> str:
204+
return self.__str__()
205+
206+
72207
class CodeScanning:
73208
"""Code Scanning."""
74209

75210
def __init__(
76211
self,
77212
repository: Optional[Repository] = None,
78213
retry_count: int = 1,
79-
retry_sleep: int = 15,
214+
retry_sleep: Union[int, float] = 15,
80215
) -> None:
81216
"""Code Scanning REST API.
82217
@@ -90,7 +225,7 @@ def __init__(
90225
self.repository = repository or GitHub.repository
91226
self.tools: List[str] = []
92227

93-
self.setup: Optional[dict] = None
228+
self.setup: Optional[CodeScanningConfiguration] = None
94229

95230
if not self.repository:
96231
raise GHASToolkitError("CodeScanning requires Repository to be set")
@@ -125,7 +260,7 @@ def isCodeQLDefaultSetup(self) -> bool:
125260
if not self.setup:
126261
self.setup = self.getDefaultConfiguration()
127262

128-
return self.setup.get("state", "not-configured") == "configured"
263+
return self.setup.state == "configured"
129264

130265
def enableDefaultSetup(
131266
self,
@@ -167,7 +302,7 @@ def getOrganizationAlerts(self, state: str = "open") -> list[CodeAlert]:
167302
docs="https://docs.github.com/en/rest/code-scanning#list-code-scanning-alerts-for-an-organization",
168303
)
169304

170-
def getDefaultConfiguration(self) -> dict:
305+
def getDefaultConfiguration(self) -> CodeScanningConfiguration:
171306
"""Get Default Code Scanning Configuration.
172307
173308
Permissions:
@@ -177,7 +312,7 @@ def getDefaultConfiguration(self) -> dict:
177312
"""
178313
result = self.rest.get("/repos/{owner}/{repo}/code-scanning/default-setup")
179314
if isinstance(result, dict):
180-
self.setup = result
315+
self.setup = loadOctoItem(CodeScanningConfiguration, result)
181316
return self.setup
182317

183318
raise GHASToolkitTypeError(
@@ -239,11 +374,7 @@ def getAlertsInPR(self, base: str) -> list[CodeAlert]:
239374
# Try merge and then head
240375
analysis = self.getAnalyses(reference=self.repository.reference)
241376
if len(analysis) == 0:
242-
analysis = self.getAnalyses(
243-
reference=self.repository.reference.replace("/merge", "/head")
244-
)
245-
if len(analysis) == 0:
246-
raise GHASToolkitError("No analyses found for the PR")
377+
raise GHASToolkitError("No analyses found for the PR")
247378

248379
# For CodeQL results using Default Setup
249380
reference = analysis[0].get("ref")
@@ -299,7 +430,7 @@ def getAlertInstances(
299430

300431
def getAnalyses(
301432
self, reference: Optional[str] = None, tool: Optional[str] = None
302-
) -> list[dict]:
433+
) -> list[CodeScanningAnalysis]:
303434
"""Get a list of all the analyses for a given repository.
304435
305436
This function will retry X times with a Y second sleep between each retry to
@@ -315,13 +446,23 @@ def getAnalyses(
315446
316447
https://docs.github.com/en/enterprise-cloud@latest/rest/code-scanning#list-code-scanning-analyses-for-a-repository
317448
"""
449+
ref = reference or self.repository.reference
450+
logger.debug(f"Getting Analyses for {ref}")
451+
if ref is None:
452+
raise GHASToolkitError("Reference is required for getting analyses")
453+
318454
counter = 0
455+
456+
logger.debug(
457+
f"Fetching Analyses (retries {self.retry_count} every {self.retry_sleep}s)"
458+
)
459+
319460
while counter < self.retry_count:
320461
counter += 1
321462

322463
results = self.rest.get(
323464
"/repos/{org}/{repo}/code-scanning/analyses",
324-
{"tool_name": tool, "ref": reference or self.repository.reference},
465+
{"tool_name": tool, "ref": ref},
325466
)
326467
if not isinstance(results, list):
327468
raise GHASToolkitTypeError(
@@ -332,13 +473,37 @@ def getAnalyses(
332473
docs="https://docs.github.com/en/enterprise-cloud@latest/rest/code-scanning#list-code-scanning-analyses-for-a-repository",
333474
)
334475

335-
if len(results) > 0 and self.retry_count > 1:
336-
logger.info(
337-
f"No analyses found, retrying {counter}/{self.retry_count})"
476+
# Try default setup `head` if no results (required for default setup)
477+
if (
478+
len(results) == 0
479+
and self.repository.isInPullRequest()
480+
and (ref.endswith("/merge") or ref.endswith("/head"))
481+
):
482+
logger.debug("No analyses found for `merge`, trying `head`")
483+
results = self.rest.get(
484+
"/repos/{org}/{repo}/code-scanning/analyses",
485+
{"tool_name": tool, "ref": ref.replace("/merge", "/head")},
338486
)
339-
time.sleep(self.retry_sleep)
487+
if not isinstance(results, list):
488+
raise GHASToolkitTypeError(
489+
"Error getting analyses from Repository",
490+
permissions=[
491+
'"Code scanning alerts" repository permissions (read)'
492+
],
493+
docs="https://docs.github.com/en/enterprise-cloud@latest/rest/code-scanning#list-code-scanning-analyses-for-a-repository",
494+
)
495+
496+
if len(results) < 0:
497+
# If the retry count is less than 1, we don't retry
498+
if self.retry_count < 1:
499+
logger.debug(
500+
f"No analyses found, retrying {counter}/{self.retry_count})"
501+
)
502+
time.sleep(self.retry_sleep)
340503
else:
341-
return results
504+
return [
505+
loadOctoItem(CodeScanningAnalysis, analysis) for analysis in results
506+
]
342507

343508
# If we get here, we have retried the max number of times and still no results
344509
raise GHASToolkitError(
@@ -349,7 +514,7 @@ def getAnalyses(
349514

350515
def getLatestAnalyses(
351516
self, reference: Optional[str] = None, tool: Optional[str] = None
352-
) -> list[dict]:
517+
) -> list[CodeScanningAnalysis]:
353518
"""Get Latest Analyses for every tool.
354519
355520
Permissions:
@@ -361,16 +526,31 @@ def getLatestAnalyses(
361526
results = []
362527

363528
for analysis in self.getAnalyses(reference, tool):
364-
name = analysis.get("tool", {}).get("name")
365-
if name in tools:
529+
if analysis.tool.name in tools:
366530
continue
367-
tools.add(name)
531+
tools.add(analysis.tool.name)
368532
results.append(analysis)
369533

370534
self.tools = list(tools)
371535

372536
return results
373537

538+
def getFailedAnalyses(
539+
self, reference: Optional[str] = None
540+
) -> list[CodeScanningAnalysis]:
541+
"""Get Failed Analyses for a given reference. This will return all analyses with errors or warnings.
542+
543+
Permissions:
544+
- "Code scanning alerts" repository permissions (read)
545+
546+
https://docs.github.com/en/rest/code-scanning/code-scanning
547+
"""
548+
return [
549+
analysis
550+
for analysis in self.getAnalyses(reference)
551+
if analysis.error != "" or analysis.warning != ""
552+
]
553+
374554
def getTools(self, reference: Optional[str] = None) -> List[str]:
375555
"""Get list of tools from the latest analyses.
376556

src/ghastoolkit/utils/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def parse_args(self) -> Namespace:
119119
# GitHub Init
120120
GitHub.init(
121121
repository=arguments.repository,
122+
reference=arguments.ref,
122123
owner=arguments.owner,
123124
instance=arguments.instance,
124125
token=arguments.token,

0 commit comments

Comments
 (0)