Skip to content

Commit

Permalink
Merge pull request #9 from Santandersecurityresearch/dev
Browse files Browse the repository at this point in the history
Merge release 1.1.0 to main
  • Loading branch information
emilejq committed Jan 25, 2024
2 parents 7c64f91 + 81fe16a commit 4197d51
Show file tree
Hide file tree
Showing 36 changed files with 2,128 additions and 239 deletions.
2 changes: 2 additions & 0 deletions .gitchangelog.rc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
##
## 'refactor' is obviously for refactoring code only
## 'minor' is for a very meaningless change (a typo, adding a comment)
## 'tests' is for a change only to the tests
## 'wip' is for partial functionality but complete sub-functionality
## 'ignore' for any other commit that should be excluded from the changelog
## 'deprecate' is for deprecating things
Expand Down Expand Up @@ -54,6 +55,7 @@ ignore_regexps = [
r'!ignore',
r'!minor',
r'!refactor',
r'!tests',
r'!wip',
r'^$', ## empty messages
]
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Scan Pull Request

on:
pull_request:
branches:
- dev

jobs:
scan:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
python-version: [ '3.10', '3.11', '3.12' ]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run unit tests
run: tox run -e py$version -- --junitxml results.xml

- name: Upload test results
uses: actions/upload-artifact@master
with:
name: Test results - ${{ matrix.python-version }}
path: results.xml

- name: Run linter
uses: chartboost/ruff-action@v1

- name: Run SAST
uses: chartboost/ruff-action@v1
with:
src: cbom
args: --select S
16 changes: 16 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ The format is based on `keep a changelog`_, and this project adheres to `semanti
This file is autogenerated by `gitchangelog`_. Please do not edit it manually.


1.1.0 (2024-01-25)
------------------

Changed
~~~~~~~
- Rewrite CLI using click. [emilejq]

Fixed
~~~~~
- Reduce algorithms being reported as unknown. [emilejq]
- Add aliases for Triple DES & Diffie-Hellman. [emilejq]
- Align with CodeQL CLI SARIF output format. [emilejq]
- Use region (not contextRegion) when extracting algorithm to avoid
misidentification (#7) [Matt Colman]


1.0.1 (2023-12-07)
------------------

Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ Replace {VERSION} with the specific version number you downloaded. This step equ
Generate a CBOM using the following command:

```
generate-cbom <path>

cryptobom generate <path>
```

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

By default, the output will be printed to `stdout`. You can alternatively write the output to a file using
`--output-file` or `-o`.

```shell
$ cryptobom generate <path> --output-file cbom.json
```

### Excluding File Paths

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

```shell
$ generate-cbom <path> --exclude '(.*/)?test(s)?.*'
$ cryptobom generate <path> --exclude '(.*/)?test(s)?.*'
```

## Cryptography Checker - Enhanced Cryptography Compliance Analysis
Expand Down
2 changes: 1 addition & 1 deletion cbom/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.1'
__version__ = '1.1.0'
Empty file added cbom/cli/__init__.py
Empty file.
107 changes: 107 additions & 0 deletions cbom/cli/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import json
import pathlib
import re

import click
from click import Path
from cyclonedx.model.bom import Bom, Tool
from cyclonedx.model.component import Component, ComponentType
from cyclonedx.output.json import JsonV1Dot4CbomV1Dot0

from cbom import __version__
from cbom.cryptocheck import cryptocheck
from cbom.parser import algorithm


@click.group(context_settings={
'max_content_width': 120,
'show_default': True
})
@click.version_option(__version__, '--version', '-v')
def cryptobom():
"""\b
_ _ __ \b
| | | | / _| \b
___ _ __ _ _ _ __ | |_ ___ | |__ ___ _ __ ___ | |_ ___ _ __ __ _ ___ \b
/ __|| '__|| | | || '_ \ | __| / _ \ | '_ \ / _ \ | '_ ` _ \ ______ | _| / _ \ | '__| / _` | / _ \ \b
| (__ | | | |_| || |_) || |_ | (_) || |_) || (_) || | | | | ||______|| | | (_) || | | (_| || __/ \b
\___||_| \__, || .__/ \__| \___/ |_.__/ \___/ |_| |_| |_| |_| \___/ |_| \__, | \___| \b
__/ || | __/ | \b
|___/ |_| |___/

Welcome to cryptobom-forge!

This script is intended to be used in conjunction with the SARIF output from the CodeQL cryptography experimental
queries.

You can use this script to generate a cryptographic bill of materials (CBOM) for a repository, and analyse your
cryptographic inventory for weak and non-pqc-safe cryptography.
"""


@cryptobom.command(context_settings={
'default_map': {
'application_name': 'root',
'cryptocheck_output_file': 'cryptocheck.sarif'
}
})
@click.argument('path', type=Path(exists=True, path_type=pathlib.Path), required=True)
@click.option('--application-name', '-n', help='Root application name')
@click.option('--cryptocheck', '-cc', 'enable_cryptocheck', is_flag=True, help='Enable crypto vulnerability scanning')
@click.option('--exclude', '-e', 'exclusion_pattern', metavar='REGEX', help='Exclude CodeQL findings in file paths that match <REGEX>')
@click.option('--output-file', '-o', help='CBOM output file')
@click.option('--rules-file', '-r', type=Path(exists=True, path_type=pathlib.Path), help='Custom ruleset for cryptocheck analysis')
@click.option('--cryptocheck-output-file', help='Cryptocheck analysis output file')
def generate(path, application_name, enable_cryptocheck, exclusion_pattern, output_file, rules_file, cryptocheck_output_file):
"""Generate a CBOM from CodeQL SARIF output."""
cbom = Bom()
cbom.metadata.component = Component(name=application_name, type=ComponentType.APPLICATION)

if exclusion_pattern:
exclusion_pattern = re.compile(exclusion_pattern)

if path.is_file():
_process_file(cbom, path, exclusion_pattern=exclusion_pattern)
else:
for file_path in [*list(path.glob('*.sarif')), *list(path.glob('*.json'))]:
_process_file(cbom, file_path, exclusion_pattern=exclusion_pattern)

if enable_cryptocheck:
cryptocheck_output = cryptocheck.validate_cbom(cbom, rules_file)
with open(cryptocheck_output_file, 'w') as file:
click.echo(message=json.dumps(cryptocheck_output, indent=4, sort_keys=True), file=file)

cbom = json.loads(JsonV1Dot4CbomV1Dot0(cbom).output_as_string(bom_format='CBOM'))
if output_file:
with open(output_file, 'w') as file:
click.echo(message=json.dumps(cbom, indent=4), file=file)
else:
click.echo(message=json.dumps(cbom, indent=4))


def start():
try:
cryptobom()
except Exception as e:
click.secho(str(e), fg='red')


def _process_file(cbom, query_file, exclusion_pattern=None):
with open(query_file) as query_output:
query_output = json.load(query_output)['runs'][0]

driver = query_output['tool']['driver']
cbom.metadata.tools.add(Tool(
vendor=driver['organization'],
name=driver['name'],
version=driver.get('version', driver.get('semanticVersion')) # fixme: sarif misaligned
))

for result in query_output['results']:
result = result['locations'][0]['physicalLocation']
if not exclusion_pattern or not exclusion_pattern.fullmatch(result['artifactLocation']['uri']):
algorithm.parse_algorithm(cbom, result)


if __name__ == '__main__':
start()
4 changes: 0 additions & 4 deletions cbom/lib_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,3 @@ def get_padding_schemes():

def get_primitive_mapping(algorithm):
return CaseInsensitiveDict(YAML_LIBRARY['crypto']['primitive-mappings']).get(algorithm, 'unknown')


def get_query_mapping(rule_id):
return CaseInsensitiveDict(YAML_LIBRARY['codeql']['query-mappings']).get(rule_id)
94 changes: 0 additions & 94 deletions cbom/main.py

This file was deleted.

25 changes: 14 additions & 11 deletions cbom/parser/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
_PADDING_REGEX = re.compile(f"{'|'.join(lib_utils.get_padding_schemes())}", flags=re.IGNORECASE)


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

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

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


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

if algorithm.lower() == 'fernet':
if algorithm == 'unknown':
algorithm = utils.get_algorithm(code_snippet)

if algorithm == 'FERNET':
algorithm, key_size, mode = 'AES', '128', Mode.CBC
primitive = Primitive.BLOCK_CIPHER
else:
Expand Down Expand Up @@ -79,9 +82,9 @@ def _generate_crypto_component(codeql_result):
variant=_build_variant(algorithm, key_size=key_size, block_mode=mode),
mode=mode,
padding=padding,
crypto_functions=_extract_crypto_functions(codeql_result['locations'][0]['physicalLocation']['contextRegion']['snippet']['text'])
crypto_functions=_extract_crypto_functions(code_snippet)
),
detection_context=utils.get_detection_contexts(locations=codeql_result['locations'])
detection_context=[utils.get_detection_context(finding)]
)


Expand Down
Loading

0 comments on commit 4197d51

Please sign in to comment.