Skip to content

Commit 8610072

Browse files
committed
feat(sca): move SCA models from ggshield to pygitguardian
1 parent f413ff1 commit 8610072

8 files changed

+853
-1
lines changed

pygitguardian/client.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
SecretScanPreferences,
3333
ServerMetadata,
3434
)
35+
from .sca_models import (
36+
ComputeSCAFilesResult,
37+
SCAScanAllOutput,
38+
SCAScanDiffOutput,
39+
SCAScanParameters,
40+
)
3541

3642

3743
logger = logging.getLogger(__name__)
@@ -605,3 +611,89 @@ def create_jwt(
605611
obj = load_detail(resp)
606612
obj.status_code = resp.status_code
607613
return obj
614+
615+
def compute_sca_files(
616+
self,
617+
files: List[str],
618+
extra_headers: Optional[Dict[str, str]] = None,
619+
) -> Union[Detail, ComputeSCAFilesResult]:
620+
if len(files) == 0:
621+
result = ComputeSCAFilesResult(sca_files=[], potential_siblings=[])
622+
result.status_code = 200
623+
return result
624+
625+
response = self.post(
626+
endpoint="sca/compute_sca_files/",
627+
data={"files": files},
628+
extra_headers=extra_headers,
629+
)
630+
result: Union[Detail, ComputeSCAFilesResult]
631+
if is_ok(response):
632+
result = ComputeSCAFilesResult.from_dict(response.json())
633+
else:
634+
result = load_detail(response)
635+
636+
result.status_code = response.status_code
637+
return result
638+
639+
def sca_scan_directory(
640+
self,
641+
tar_file: bytes,
642+
scan_parameters: SCAScanParameters,
643+
extra_headers: Optional[Dict[str, str]] = None,
644+
) -> Union[Detail, SCAScanAllOutput]:
645+
"""
646+
Launches an SCA scan via SCA public API on a tar archive
647+
"""
648+
649+
result: Union[Detail, SCAScanAllOutput]
650+
651+
try:
652+
# bypass self.post because data argument is needed in self.request and self.post use it as json
653+
response = self.request(
654+
"post",
655+
endpoint="sca/sca_scan_all/",
656+
files={"directory": tar_file},
657+
data={
658+
"scan_parameters": SCAScanParameters.SCHEMA.dumps(scan_parameters)
659+
},
660+
extra_headers=extra_headers,
661+
)
662+
except requests.exceptions.ReadTimeout:
663+
result = Detail("The request timed out.")
664+
result.status_code = 504
665+
else:
666+
if is_ok(response):
667+
result = SCAScanAllOutput.from_dict(response.json())
668+
else:
669+
result = load_detail(response)
670+
671+
result.status_code = response.status_code
672+
673+
return result
674+
675+
def scan_diff(
676+
self,
677+
reference: bytes,
678+
current: bytes,
679+
scan_parameters: SCAScanParameters,
680+
) -> Union[Detail, SCAScanDiffOutput]:
681+
result: Union[Detail, SCAScanDiffOutput]
682+
try:
683+
response = self.post(
684+
endpoint="sca/sca_scan_diff/",
685+
files={"reference": reference, "current": current},
686+
data={
687+
"scan_parameters": SCAScanParameters.SCHEMA.dumps(scan_parameters)
688+
},
689+
)
690+
except requests.exceptions.ReadTimeout:
691+
result = Detail("The request timed out.")
692+
result.status_code = 504
693+
else:
694+
if is_ok(response):
695+
result = SCAScanDiffOutput.from_dict(response.json())
696+
else:
697+
result = load_detail(response)
698+
result.status_code = response.status_code
699+
return result

pygitguardian/sca_models.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from dataclasses import dataclass, field
2+
from datetime import datetime
3+
from typing import List, Optional, cast
4+
5+
import marshmallow_dataclass
6+
from typing_extensions import Literal
7+
8+
from pygitguardian.models import Base, BaseSchema, FromDictMixin
9+
10+
11+
@dataclass
12+
class SCAIgnoredVulnerability(Base, FromDictMixin):
13+
"""
14+
A model of an ignored vulnerability for SCA. This allows to ignore all occurrences
15+
of a given vulnerability in a given dependency file.
16+
- identifier: identifier (currently: GHSA id) of the vulnerability to ignore
17+
- path: the path to the file in which ignore the vulnerability
18+
"""
19+
20+
identifier: str
21+
path: str
22+
23+
24+
SCAIgnoredVulnerability.SCHEMA = cast(
25+
BaseSchema,
26+
marshmallow_dataclass.class_schema(
27+
SCAIgnoredVulnerability, base_schema=BaseSchema
28+
)(),
29+
)
30+
31+
32+
@dataclass
33+
class SCAScanParameters(Base, FromDictMixin):
34+
minimum_severity: Optional[str] = None
35+
ignored_vulnerabilities: List[SCAIgnoredVulnerability] = field(default_factory=list)
36+
37+
38+
SCAScanParameters.SCHEMA = cast(
39+
BaseSchema,
40+
marshmallow_dataclass.class_schema(SCAScanParameters, base_schema=BaseSchema)(),
41+
)
42+
43+
44+
@dataclass
45+
class ComputeSCAFilesResult(Base, FromDictMixin):
46+
sca_files: List[str]
47+
potential_siblings: List[str]
48+
49+
50+
ComputeSCAFilesResult.SCHEMA = cast(
51+
BaseSchema,
52+
marshmallow_dataclass.class_schema(ComputeSCAFilesResult, base_schema=BaseSchema)(),
53+
)
54+
55+
56+
@dataclass
57+
class SCAVulnerability(Base, FromDictMixin):
58+
severity: str
59+
summary: str
60+
identifier: str
61+
cve_ids: List[str] = field(default_factory=list)
62+
created_at: Optional[datetime] = None
63+
fixed_version: Optional[str] = None
64+
65+
66+
SCAVulnerability.SCHEMA = cast(
67+
BaseSchema,
68+
marshmallow_dataclass.class_schema(SCAVulnerability, base_schema=BaseSchema)(),
69+
)
70+
71+
SCADependencyType = Literal["direct", "transitive"]
72+
73+
74+
@dataclass
75+
class SCAVulnerablePackageVersion(Base, FromDictMixin):
76+
package_full_name: str
77+
version: str
78+
ecosystem: str
79+
dependency_type: Optional[SCADependencyType] = None
80+
vulns: List[SCAVulnerability] = field(default_factory=list)
81+
82+
83+
SCAVulnerablePackageVersion.SCHEMA = cast(
84+
BaseSchema,
85+
marshmallow_dataclass.class_schema(
86+
SCAVulnerablePackageVersion, base_schema=BaseSchema
87+
)(),
88+
)
89+
90+
91+
@dataclass
92+
class SCALocationVulnerability(Base, FromDictMixin):
93+
location: str
94+
package_vulns: List[SCAVulnerablePackageVersion] = field(default_factory=list)
95+
96+
97+
SCALocationVulnerability.SCHEMA = cast(
98+
BaseSchema,
99+
marshmallow_dataclass.class_schema(
100+
SCALocationVulnerability, base_schema=BaseSchema
101+
)(),
102+
)
103+
104+
105+
@dataclass
106+
class SCAScanAllOutput(Base, FromDictMixin):
107+
scanned_files: List[str] = field(default_factory=list)
108+
found_package_vulns: List[SCALocationVulnerability] = field(default_factory=list)
109+
110+
111+
SCAScanAllOutput.SCHEMA = cast(
112+
BaseSchema,
113+
marshmallow_dataclass.class_schema(SCAScanAllOutput, base_schema=BaseSchema)(),
114+
)
115+
116+
117+
@dataclass
118+
class SCAScanDiffOutput(Base, FromDictMixin):
119+
scanned_files: List[str] = field(default_factory=list)
120+
added_vulns: List[SCALocationVulnerability] = field(default_factory=list)
121+
removed_vulns: List[SCALocationVulnerability] = field(default_factory=list)
122+
123+
124+
SCAScanDiffOutput.SCHEMA = cast(
125+
BaseSchema,
126+
marshmallow_dataclass.class_schema(SCAScanDiffOutput, base_schema=BaseSchema)(),
127+
)

tests/cassettes/test_sca_client_scan_diff.yaml

Lines changed: 93 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
interactions:
2+
- request:
3+
body: '{"files": ["Pipfile", "something_else"]}'
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Content-Length:
12+
- '40'
13+
Content-Type:
14+
- application/json
15+
User-Agent:
16+
- pygitguardian/1.9.0 (Linux;py3.10.12)
17+
method: POST
18+
uri: https://api.gitguardian.com/v1/sca/compute_sca_files/
19+
response:
20+
body:
21+
string: '{"sca_files":["Pipfile"],"potential_siblings":["Pipfile.lock"]}'
22+
headers:
23+
access-control-expose-headers:
24+
- X-App-Version
25+
allow:
26+
- POST, OPTIONS
27+
content-length:
28+
- '63'
29+
content-type:
30+
- application/json
31+
cross-origin-opener-policy:
32+
- same-origin
33+
date:
34+
- Thu, 17 Aug 2023 08:43:29 GMT
35+
referrer-policy:
36+
- strict-origin-when-cross-origin
37+
server:
38+
- istio-envoy
39+
strict-transport-security:
40+
- max-age=31536000; includeSubDomains
41+
vary:
42+
- Cookie
43+
x-app-version:
44+
- v2.36.1
45+
x-content-type-options:
46+
- nosniff
47+
- nosniff
48+
x-envoy-upstream-service-time:
49+
- '15'
50+
x-frame-options:
51+
- DENY
52+
- SAMEORIGIN
53+
x-sca-engine-version:
54+
- 1.16.1
55+
x-secrets-engine-version:
56+
- 2.95.0
57+
x-xss-protection:
58+
- 1; mode=block
59+
status:
60+
code: 200
61+
message: OK
62+
version: 1
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
interactions:
2+
- request:
3+
body:
4+
"--770ddecb25300ec91a085b90915fe8f1\r\nContent-Disposition: form-data; name=\"scan_parameters\"\r\n\r\n{\"minimum_severity\":
5+
null, \"ignored_vulnerabilities\": []}\r\n--770ddecb25300ec91a085b90915fe8f1\r\nContent-Disposition:
6+
form-data; name=\"directory\"; filename=\"directory\"\r\n\r\n\r\n--770ddecb25300ec91a085b90915fe8f1--\r\n"
7+
headers:
8+
Accept:
9+
- '*/*'
10+
Accept-Encoding:
11+
- gzip, deflate
12+
Connection:
13+
- keep-alive
14+
Content-Length:
15+
- '303'
16+
Content-Type:
17+
- multipart/form-data; boundary=770ddecb25300ec91a085b90915fe8f1
18+
User-Agent:
19+
- pygitguardian/1.9.0 (Linux;py3.10.12)
20+
method: POST
21+
uri: https://api.gitguardian.com/v1/sca/sca_scan_all/
22+
response:
23+
body:
24+
string: '{"detail":"Directory is not a valid tarfile"}'
25+
headers:
26+
access-control-expose-headers:
27+
- X-App-Version
28+
allow:
29+
- POST, OPTIONS
30+
content-length:
31+
- '45'
32+
content-type:
33+
- application/json
34+
cross-origin-opener-policy:
35+
- same-origin
36+
date:
37+
- Thu, 17 Aug 2023 08:43:30 GMT
38+
referrer-policy:
39+
- strict-origin-when-cross-origin
40+
server:
41+
- istio-envoy
42+
strict-transport-security:
43+
- max-age=31536000; includeSubDomains
44+
vary:
45+
- Cookie
46+
x-app-version:
47+
- v2.36.1
48+
x-content-type-options:
49+
- nosniff
50+
- nosniff
51+
x-envoy-upstream-service-time:
52+
- '16'
53+
x-frame-options:
54+
- DENY
55+
x-sca-engine-version:
56+
- 1.16.1
57+
x-secrets-engine-version:
58+
- 2.95.0
59+
x-xss-protection:
60+
- 1; mode=block
61+
status:
62+
code: 400
63+
message: Bad Request
64+
version: 1

tests/cassettes/test_sca_scan_directory_valid.yaml

Lines changed: 89 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)