Skip to content

Commit e033971

Browse files
committed
Add: Extend GitHub code scanning API for handling SARIF data
Implement the two missing APIs for the GitHub code scanning API handling SARIF data.
1 parent 4607a2e commit e033971

File tree

4 files changed

+279
-0
lines changed

4 files changed

+279
-0
lines changed

pontos/github/api/code_scanning.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
#
33
# SPDX-License-Identifier: GPL-3.0-or-later
44

5+
import base64
6+
import gzip
7+
from datetime import datetime
58
from typing import AsyncIterator, Iterable, Optional, Union
69

710
from pontos.github.api.client import GitHubAsyncREST
11+
from pontos.github.api.helper import JSON_OBJECT
812
from pontos.github.models.base import SortOrder
913
from pontos.github.models.code_scanning import (
1014
AlertSort,
@@ -18,6 +22,7 @@
1822
Instance,
1923
Language,
2024
QuerySuite,
25+
SarifUploadInformation,
2126
Severity,
2227
)
2328
from pontos.helper import enum_or_value
@@ -630,3 +635,118 @@ async def update_default_setup(
630635
response = await self._client.patch(api, data=data)
631636
response.raise_for_status()
632637
return response.json()
638+
639+
async def upload_sarif_data(
640+
self,
641+
repo: str,
642+
commit_sha: str,
643+
ref: str,
644+
sarif: bytes,
645+
*,
646+
checkout_uri: Optional[str] = None,
647+
started_at: Optional[datetime] = None,
648+
tool_name: Optional[str] = None,
649+
validate: Optional[bool] = None,
650+
) -> dict[str, str]:
651+
"""
652+
Upload SARIF data containing the results of a code scanning analysis to
653+
make the results available in a repository
654+
655+
https://docs.github.com/en/rest/code-scanning/code-scanning#upload-an-analysis-as-sarif-data
656+
657+
Args:
658+
repo: GitHub repository (owner/name)
659+
commit_sha: The SHA of the commit to which the analysis you are
660+
uploading relates
661+
ref: The full Git reference, formatted as refs/heads/<branch name>,
662+
refs/pull/<number>/merge, or refs/pull/<number>/head
663+
sarif:
664+
checkout_uri: The base directory used in the analysis, as it appears
665+
in the SARIF file
666+
started_at: The time that the analysis run began
667+
tool_name: The name of the tool used to generate the code scanning
668+
analysis
669+
validate: Whether the SARIF file will be validated according to the
670+
code scanning specifications
671+
672+
Raises:
673+
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
674+
failed.
675+
676+
Returns:
677+
See the GitHub documentation for the response object
678+
679+
Example:
680+
.. code-block:: python
681+
682+
from pathlib import Path
683+
from pontos.github.api import GitHubAsyncRESTApi
684+
685+
async with GitHubAsyncRESTApi(token) as api:
686+
json = await api.code_scanning.upload_sarif_data(
687+
"org/repo",
688+
commit_sha="4b6472266afd7b471e86085a6659e8c7f2b119da",
689+
ref="refs/heads/main",
690+
sarif=Path("/path/to/sarif.file").read_bytes(),
691+
)
692+
print(json["id"])
693+
"""
694+
api = f"/repos/{repo}/code-scanning/sarifs"
695+
data: JSON_OBJECT = {
696+
"commit_sha": commit_sha,
697+
"ref": ref,
698+
}
699+
if checkout_uri:
700+
data["checkout_uri"] = checkout_uri
701+
if started_at:
702+
data["started_at"] = started_at.isoformat(timespec="seconds")
703+
if tool_name:
704+
data["tool_name"] = tool_name
705+
if validate is not None:
706+
data["validate"] = validate
707+
708+
compressed = gzip.compress(sarif, mtime=0)
709+
encoded = base64.b64encode(compressed).decode(encoding="ascii")
710+
711+
data["sarif"] = encoded
712+
713+
response = await self._client.post(api, data=data)
714+
response.raise_for_status()
715+
return response.json()
716+
717+
async def sarif(self, repo: str, sarif_id: str) -> SarifUploadInformation:
718+
"""
719+
Gets information about a SARIF upload, including the status and the URL
720+
of the analysis that was uploaded so that you can retrieve details of
721+
the analysis
722+
723+
https://docs.github.com/en/rest/code-scanning/code-scanning#get-information-about-a-sarif-upload
724+
725+
Args:
726+
repo: GitHub repository (owner/name)
727+
sarif_id: The SARIF ID obtained after uploading
728+
729+
Raises:
730+
HTTPStatusError: A httpx.HTTPStatusError is raised if the request
731+
failed.
732+
733+
Returns:
734+
Information about the SARIF upload
735+
736+
Example:
737+
.. code-block:: python
738+
739+
from pontos.github.api import GitHubAsyncRESTApi
740+
741+
async with GitHubAsyncRESTApi(token) as api:
742+
sarif = await api.code_scanning.sarif(
743+
"org/repo",
744+
"47177e22-5596-11eb-80a1-c1e54ef945c6",
745+
)
746+
print(sarif)
747+
"""
748+
api = f"/repos/{repo}/code-scanning/sarifs/{sarif_id}"
749+
750+
response = await self._client.get(api)
751+
response.raise_for_status()
752+
return SarifUploadInformation.from_dict(response.json())

pontos/github/models/code_scanning.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,32 @@ class DefaultSetup(GitHubModel):
368368
query_suite: QuerySuite
369369
updated_at: Optional[datetime] = None
370370
schedule: Optional[str] = None
371+
372+
373+
class SarifProcessingStatus(Enum):
374+
"""
375+
`pending` files have not yet been processed, while `complete` means results
376+
from the SARIF have been stored. `failed` files have either not been
377+
processed at all, or could only be partially processed
378+
"""
379+
380+
PENDING = "pending"
381+
COMPLETE = "complete"
382+
FAILED = "failed"
383+
384+
385+
@dataclass
386+
class SarifUploadInformation(GitHubModel):
387+
"""
388+
Information about the SARIF upload
389+
390+
Attributes:
391+
processing_status: Status of the SARIF processing
392+
analyses_url: The REST API URL for getting the analyses associated with
393+
the upload
394+
errors: Any errors that ocurred during processing of the delivery
395+
"""
396+
397+
processing_status: SarifProcessingStatus
398+
analyses_url: Optional[str] = None
399+
errors: Optional[list[str]] = None

tests/github/api/test_code_scanning.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
# ruff: noqa:E501
66

7+
import json
8+
79
from pontos.github.api.code_scanning import GitHubAsyncRESTCodeScanning
810
from pontos.github.models.base import SortOrder
911
from pontos.github.models.code_scanning import (
@@ -13,6 +15,7 @@
1315
DismissedReason,
1416
Language,
1517
QuerySuite,
18+
SarifProcessingStatus,
1619
Severity,
1720
)
1821
from tests import AsyncIteratorMock, aiter, anext
@@ -1251,3 +1254,110 @@ async def test_update_default_setup(self):
12511254
resp["run_url"],
12521255
"https://api.github.com/repos/octoorg/octocat/actions/runs/42",
12531256
)
1257+
1258+
async def test_upload_sarif_data(self):
1259+
sarif = {
1260+
"version": "2.1.0",
1261+
"$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4",
1262+
"runs": [
1263+
{
1264+
"tool": {
1265+
"driver": {
1266+
"name": "ESLint",
1267+
"informationUri": "https://eslint.org",
1268+
"rules": [
1269+
{
1270+
"id": "no-unused-vars",
1271+
"shortDescription": {
1272+
"text": "disallow unused variables"
1273+
},
1274+
"helpUri": "https://eslint.org/docs/rules/no-unused-vars",
1275+
"properties": {"category": "Variables"},
1276+
}
1277+
],
1278+
}
1279+
},
1280+
"artifacts": [
1281+
{
1282+
"location": {
1283+
"uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js"
1284+
}
1285+
}
1286+
],
1287+
"results": [
1288+
{
1289+
"level": "error",
1290+
"message": {
1291+
"text": "'x' is assigned a value but never used."
1292+
},
1293+
"locations": [
1294+
{
1295+
"physicalLocation": {
1296+
"artifactLocation": {
1297+
"uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js",
1298+
"index": 0,
1299+
},
1300+
"region": {
1301+
"startLine": 1,
1302+
"startColumn": 5,
1303+
},
1304+
}
1305+
}
1306+
],
1307+
"ruleId": "no-unused-vars",
1308+
"ruleIndex": 0,
1309+
}
1310+
],
1311+
}
1312+
],
1313+
}
1314+
1315+
response = create_response()
1316+
response.json.return_value = {
1317+
"id": "47177e22-5596-11eb-80a1-c1e54ef945c6",
1318+
"url": "https://api.github.com/repos/octocat/hello-world/code-scanning/sarifs/47177e22-5596-11eb-80a1-c1e54ef945c6",
1319+
}
1320+
self.client.post.return_value = response
1321+
1322+
resp = await self.api.upload_sarif_data(
1323+
"foo/bar",
1324+
commit_sha="4b6472266afd7b471e86085a6659e8c7f2b119da",
1325+
ref="refs/heads/master",
1326+
sarif=json.dumps(sarif).encode(),
1327+
)
1328+
self.client.post.assert_awaited_once_with(
1329+
"/repos/foo/bar/code-scanning/sarifs",
1330+
data={
1331+
"commit_sha": "4b6472266afd7b471e86085a6659e8c7f2b119da",
1332+
"ref": "refs/heads/master",
1333+
"sarif": "H4sIAAAAAAACA7VSO2/cMAz+K4JQIEttJUW73Jp2CHBbkC5FBsXm2Qpk0SCl6wUH//eSsu/aoshYaNGD/F7i2R6BOGCyO2M/tXftrf1o7AfuRpi83o05zzvnXhlTu95yRoIWaXDsKRya2tVQntrP2kslsTT+ONuMGGV3tj0FYanb5CdQ2G+P+5Cy1od0QJp8Fg1PFC6ULJzAUWqUacWNsAGHXssSNiUVhr45emIt4REpfwXuKMx59SQq4JS1vA/sY8SfZm0y0hT8i2Iu0jpCnN+ldz127KoA9y/rTDgD5VDVnW3nMwxIbwr1/TfH8rwoj5fCg+/y5iRi569Ky8p/CBGE3t3vXA/HNeQt6lwk++Ajy3maVc5DyoR96RTEcdDLBk71sX2ttBodcIlXSjiCfosFIiQ1MAGzH+CvtG5ONyaw8cxhSJKWl7xiAfNSskmCQEYzaGt2FxMbwTy+ceh83P/p7eJ7/58N14Hq4SS4t0u1PlzYOIsImTo1eqfToud7jGXS9y/LlpX88sM781XfrujPsn4BtlGkUj8DAAA=",
1334+
},
1335+
)
1336+
1337+
self.assertEqual(resp["id"], "47177e22-5596-11eb-80a1-c1e54ef945c6")
1338+
self.assertEqual(
1339+
resp["url"],
1340+
"https://api.github.com/repos/octocat/hello-world/code-scanning/sarifs/47177e22-5596-11eb-80a1-c1e54ef945c6",
1341+
)
1342+
1343+
async def test_sarif(self):
1344+
response = create_response()
1345+
response.json.return_value = {
1346+
"processing_status": "complete",
1347+
"analyses_url": "https://api.github.com/repos/octocat/hello-world/code-scanning/analyses?sarif_id=47177e22-5596-11eb-80a1-c1e54ef945c6",
1348+
}
1349+
self.client.get.return_value = response
1350+
1351+
resp = await self.api.sarif(
1352+
"foo/bar", "47177e22-5596-11eb-80a1-c1e54ef945c6"
1353+
)
1354+
self.client.get.assert_awaited_once_with(
1355+
"/repos/foo/bar/code-scanning/sarifs/47177e22-5596-11eb-80a1-c1e54ef945c6",
1356+
)
1357+
1358+
self.assertEqual(resp.processing_status, SarifProcessingStatus.COMPLETE)
1359+
self.assertEqual(
1360+
resp.analyses_url,
1361+
"https://api.github.com/repos/octocat/hello-world/code-scanning/analyses?sarif_id=47177e22-5596-11eb-80a1-c1e54ef945c6",
1362+
)
1363+
self.assertIsNone(resp.errors)

tests/github/models/test_code_scanning.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
Location,
2020
QuerySuite,
2121
Rule,
22+
SarifProcessingStatus,
23+
SarifUploadInformation,
2224
Severity,
2325
Tool,
2426
)
@@ -427,3 +429,21 @@ def test_from_dict(self):
427429
datetime(2023, 1, 19, 11, 21, 34, tzinfo=timezone.utc),
428430
)
429431
self.assertEqual(setup.schedule, "weekly")
432+
433+
434+
class SarifUploadInformationTestCase(unittest.TestCase):
435+
def test_from_dict(self):
436+
sarif = SarifUploadInformation.from_dict(
437+
{
438+
"processing_status": "complete",
439+
"analyses_url": "https://api.github.com/repos/octocat/hello-world/code-scanning/analyses?sarif_id=47177e22-5596-11eb-80a1-c1e54ef945c6",
440+
}
441+
)
442+
self.assertEqual(
443+
sarif.processing_status, SarifProcessingStatus.COMPLETE
444+
)
445+
self.assertEqual(
446+
sarif.analyses_url,
447+
"https://api.github.com/repos/octocat/hello-world/code-scanning/analyses?sarif_id=47177e22-5596-11eb-80a1-c1e54ef945c6",
448+
)
449+
self.assertIsNone(sarif.errors)

0 commit comments

Comments
 (0)