Skip to content

Commit 4197d51

Browse files
authored
Merge pull request #9 from Santandersecurityresearch/dev
Merge release 1.1.0 to main
2 parents 7c64f91 + 81fe16a commit 4197d51

36 files changed

+2128
-239
lines changed

.gitchangelog.rc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
##
2424
## 'refactor' is obviously for refactoring code only
2525
## 'minor' is for a very meaningless change (a typo, adding a comment)
26+
## 'tests' is for a change only to the tests
2627
## 'wip' is for partial functionality but complete sub-functionality
2728
## 'ignore' for any other commit that should be excluded from the changelog
2829
## 'deprecate' is for deprecating things
@@ -54,6 +55,7 @@ ignore_regexps = [
5455
r'!ignore',
5556
r'!minor',
5657
r'!refactor',
58+
r'!tests',
5759
r'!wip',
5860
r'^$', ## empty messages
5961
]

.github/workflows/pull_request.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Scan Pull Request
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- dev
7+
8+
jobs:
9+
scan:
10+
runs-on: ubuntu-latest
11+
strategy:
12+
max-parallel: 4
13+
matrix:
14+
python-version: [ '3.10', '3.11', '3.12' ]
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v4
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install -r requirements.txt
29+
30+
- name: Run unit tests
31+
run: tox run -e py$version -- --junitxml results.xml
32+
33+
- name: Upload test results
34+
uses: actions/upload-artifact@master
35+
with:
36+
name: Test results - ${{ matrix.python-version }}
37+
path: results.xml
38+
39+
- name: Run linter
40+
uses: chartboost/ruff-action@v1
41+
42+
- name: Run SAST
43+
uses: chartboost/ruff-action@v1
44+
with:
45+
src: cbom
46+
args: --select S

CHANGELOG.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ The format is based on `keep a changelog`_, and this project adheres to `semanti
77
This file is autogenerated by `gitchangelog`_. Please do not edit it manually.
88

99

10+
1.1.0 (2024-01-25)
11+
------------------
12+
13+
Changed
14+
~~~~~~~
15+
- Rewrite CLI using click. [emilejq]
16+
17+
Fixed
18+
~~~~~
19+
- Reduce algorithms being reported as unknown. [emilejq]
20+
- Add aliases for Triple DES & Diffie-Hellman. [emilejq]
21+
- Align with CodeQL CLI SARIF output format. [emilejq]
22+
- Use region (not contextRegion) when extracting algorithm to avoid
23+
misidentification (#7) [Matt Colman]
24+
25+
1026
1.0.1 (2023-12-07)
1127
------------------
1228

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ Replace {VERSION} with the specific version number you downloaded. This step equ
3939
Generate a CBOM using the following command:
4040

4141
```
42-
generate-cbom <path>
43-
42+
cryptobom generate <path>
4443
```
4544

4645
The <path> parameter is versatile, accepting either:
@@ -213,6 +212,13 @@ Upon successful execution, you will receive a detailed CBOM, structured as follo
213212
}
214213
```
215214

215+
By default, the output will be printed to `stdout`. You can alternatively write the output to a file using
216+
`--output-file` or `-o`.
217+
218+
```shell
219+
$ cryptobom generate <path> --output-file cbom.json
220+
```
221+
216222
### Excluding File Paths
217223

218224
You can optionally specify a regex string to ignore findings in files that match that path, using `--exclude` or `-e`.
@@ -221,7 +227,7 @@ The complete file path must match in order for findings to be excluded.
221227
For example, you may wish to exclude findings in test files:
222228

223229
```shell
224-
$ generate-cbom <path> --exclude '(.*/)?test(s)?.*'
230+
$ cryptobom generate <path> --exclude '(.*/)?test(s)?.*'
225231
```
226232

227233
## Cryptography Checker - Enhanced Cryptography Compliance Analysis

cbom/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.0.1'
1+
__version__ = '1.1.0'

cbom/cli/__init__.py

Whitespace-only changes.

cbom/cli/cli.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
import pathlib
3+
import re
4+
5+
import click
6+
from click import Path
7+
from cyclonedx.model.bom import Bom, Tool
8+
from cyclonedx.model.component import Component, ComponentType
9+
from cyclonedx.output.json import JsonV1Dot4CbomV1Dot0
10+
11+
from cbom import __version__
12+
from cbom.cryptocheck import cryptocheck
13+
from cbom.parser import algorithm
14+
15+
16+
@click.group(context_settings={
17+
'max_content_width': 120,
18+
'show_default': True
19+
})
20+
@click.version_option(__version__, '--version', '-v')
21+
def cryptobom():
22+
"""\b
23+
_ _ __ \b
24+
| | | | / _| \b
25+
___ _ __ _ _ _ __ | |_ ___ | |__ ___ _ __ ___ | |_ ___ _ __ __ _ ___ \b
26+
/ __|| '__|| | | || '_ \ | __| / _ \ | '_ \ / _ \ | '_ ` _ \ ______ | _| / _ \ | '__| / _` | / _ \ \b
27+
| (__ | | | |_| || |_) || |_ | (_) || |_) || (_) || | | | | ||______|| | | (_) || | | (_| || __/ \b
28+
\___||_| \__, || .__/ \__| \___/ |_.__/ \___/ |_| |_| |_| |_| \___/ |_| \__, | \___| \b
29+
__/ || | __/ | \b
30+
|___/ |_| |___/
31+
32+
Welcome to cryptobom-forge!
33+
34+
This script is intended to be used in conjunction with the SARIF output from the CodeQL cryptography experimental
35+
queries.
36+
37+
You can use this script to generate a cryptographic bill of materials (CBOM) for a repository, and analyse your
38+
cryptographic inventory for weak and non-pqc-safe cryptography.
39+
"""
40+
41+
42+
@cryptobom.command(context_settings={
43+
'default_map': {
44+
'application_name': 'root',
45+
'cryptocheck_output_file': 'cryptocheck.sarif'
46+
}
47+
})
48+
@click.argument('path', type=Path(exists=True, path_type=pathlib.Path), required=True)
49+
@click.option('--application-name', '-n', help='Root application name')
50+
@click.option('--cryptocheck', '-cc', 'enable_cryptocheck', is_flag=True, help='Enable crypto vulnerability scanning')
51+
@click.option('--exclude', '-e', 'exclusion_pattern', metavar='REGEX', help='Exclude CodeQL findings in file paths that match <REGEX>')
52+
@click.option('--output-file', '-o', help='CBOM output file')
53+
@click.option('--rules-file', '-r', type=Path(exists=True, path_type=pathlib.Path), help='Custom ruleset for cryptocheck analysis')
54+
@click.option('--cryptocheck-output-file', help='Cryptocheck analysis output file')
55+
def generate(path, application_name, enable_cryptocheck, exclusion_pattern, output_file, rules_file, cryptocheck_output_file):
56+
"""Generate a CBOM from CodeQL SARIF output."""
57+
cbom = Bom()
58+
cbom.metadata.component = Component(name=application_name, type=ComponentType.APPLICATION)
59+
60+
if exclusion_pattern:
61+
exclusion_pattern = re.compile(exclusion_pattern)
62+
63+
if path.is_file():
64+
_process_file(cbom, path, exclusion_pattern=exclusion_pattern)
65+
else:
66+
for file_path in [*list(path.glob('*.sarif')), *list(path.glob('*.json'))]:
67+
_process_file(cbom, file_path, exclusion_pattern=exclusion_pattern)
68+
69+
if enable_cryptocheck:
70+
cryptocheck_output = cryptocheck.validate_cbom(cbom, rules_file)
71+
with open(cryptocheck_output_file, 'w') as file:
72+
click.echo(message=json.dumps(cryptocheck_output, indent=4, sort_keys=True), file=file)
73+
74+
cbom = json.loads(JsonV1Dot4CbomV1Dot0(cbom).output_as_string(bom_format='CBOM'))
75+
if output_file:
76+
with open(output_file, 'w') as file:
77+
click.echo(message=json.dumps(cbom, indent=4), file=file)
78+
else:
79+
click.echo(message=json.dumps(cbom, indent=4))
80+
81+
82+
def start():
83+
try:
84+
cryptobom()
85+
except Exception as e:
86+
click.secho(str(e), fg='red')
87+
88+
89+
def _process_file(cbom, query_file, exclusion_pattern=None):
90+
with open(query_file) as query_output:
91+
query_output = json.load(query_output)['runs'][0]
92+
93+
driver = query_output['tool']['driver']
94+
cbom.metadata.tools.add(Tool(
95+
vendor=driver['organization'],
96+
name=driver['name'],
97+
version=driver.get('version', driver.get('semanticVersion')) # fixme: sarif misaligned
98+
))
99+
100+
for result in query_output['results']:
101+
result = result['locations'][0]['physicalLocation']
102+
if not exclusion_pattern or not exclusion_pattern.fullmatch(result['artifactLocation']['uri']):
103+
algorithm.parse_algorithm(cbom, result)
104+
105+
106+
if __name__ == '__main__':
107+
start()

cbom/lib_utils.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,3 @@ def get_padding_schemes():
2929

3030
def get_primitive_mapping(algorithm):
3131
return CaseInsensitiveDict(YAML_LIBRARY['crypto']['primitive-mappings']).get(algorithm, 'unknown')
32-
33-
34-
def get_query_mapping(rule_id):
35-
return CaseInsensitiveDict(YAML_LIBRARY['codeql']['query-mappings']).get(rule_id)

cbom/main.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

cbom/parser/algorithm.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
_PADDING_REGEX = re.compile(f"{'|'.join(lib_utils.get_padding_schemes())}", flags=re.IGNORECASE)
1313

1414

15-
def parse_algorithm(cbom, codeql_result):
16-
crypto_properties = _generate_crypto_component(codeql_result)
15+
def parse_algorithm(cbom, finding):
16+
crypto_properties = _generate_crypto_component(finding)
1717
if (padding := crypto_properties.algorithm_properties.padding) not in [Padding.OTHER, Padding.UNKNOWN]:
1818
name = f'{crypto_properties.algorithm_properties.variant}-{padding.value.upper()}'
1919
else:
@@ -33,21 +33,24 @@ def parse_algorithm(cbom, codeql_result):
3333
algorithm_component = _update_existing_component(existing_component, algorithm_component)
3434

3535
if crypto_properties.algorithm_properties.primitive == Primitive.PUBLIC_KEY_ENCRYPTION:
36-
code_snippet = codeql_result['locations'][0]['physicalLocation']['contextRegion']['snippet']['text']
36+
code_snippet = finding['contextRegion']['snippet']['text']
3737
if 'key' in code_snippet.lower():
38-
private_key_component = related_crypto_material.parse_private_key(cbom, codeql_result)
38+
private_key_component = related_crypto_material.parse_private_key(cbom, finding)
3939
cbom.register_dependency(algorithm_component, depends_on=[private_key_component])
4040

4141
if 'x509' in code_snippet.lower() or 'x.509' in code_snippet.lower():
42-
certificate_component = certificate.parse_x509_certificate_details(cbom, codeql_result)
42+
certificate_component = certificate.parse_x509_certificate_details(cbom, finding)
4343
cbom.register_dependency(algorithm_component, depends_on=[certificate_component])
4444

4545

46-
def _generate_crypto_component(codeql_result):
47-
code_snippet = codeql_result['locations'][0]['physicalLocation']['contextRegion']['snippet']['text']
48-
algorithm = utils.get_algorithm(code_snippet)
46+
def _generate_crypto_component(finding):
47+
code_snippet = finding['contextRegion']['snippet']['text']
48+
algorithm = utils.get_algorithm(utils.extract_precise_snippet(code_snippet, finding['region']))
4949

50-
if algorithm.lower() == 'fernet':
50+
if algorithm == 'unknown':
51+
algorithm = utils.get_algorithm(code_snippet)
52+
53+
if algorithm == 'FERNET':
5154
algorithm, key_size, mode = 'AES', '128', Mode.CBC
5255
primitive = Primitive.BLOCK_CIPHER
5356
else:
@@ -79,9 +82,9 @@ def _generate_crypto_component(codeql_result):
7982
variant=_build_variant(algorithm, key_size=key_size, block_mode=mode),
8083
mode=mode,
8184
padding=padding,
82-
crypto_functions=_extract_crypto_functions(codeql_result['locations'][0]['physicalLocation']['contextRegion']['snippet']['text'])
85+
crypto_functions=_extract_crypto_functions(code_snippet)
8386
),
84-
detection_context=utils.get_detection_contexts(locations=codeql_result['locations'])
87+
detection_context=[utils.get_detection_context(finding)]
8588
)
8689

8790

0 commit comments

Comments
 (0)