Skip to content

Commit

Permalink
Merge branch 'cwe-field' into 'main'
Browse files Browse the repository at this point in the history
Cwe field

See merge request reportcreator/reportcreator!445
  • Loading branch information
MWedl committed Feb 12, 2024
2 parents 179a23d + 7219ca8 commit c6b2848
Show file tree
Hide file tree
Showing 22 changed files with 5,887 additions and 17 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@


## Next
* Add CWE field type
* Break text in tables to prevent tables overflowing page in base styles
* UI: Add hint how to add custom tags
* UI: Add buttons for task list and footnote to markdown editor toolbar
* Sync updated field default values to preview data fields
* Automatically close brackets and enclose selected text with brackets in markdown editor
* UI: Add hint how to add custom tags
* UI: Add buttons for task list and footnote to markdown editor toolbar
* Fix text selection in markdown preview focus changed to editor
* Fix object field properties not always sorted
* Fix newline not inserted at empty last line of markdown editor in Firefox
Expand Down
6 changes: 6 additions & 0 deletions api/src/reportcreator_api/api_utils/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from reportcreator_api.api_utils.healthchecks import run_healthchecks
from reportcreator_api.api_utils.permissions import IsSystemUser, IsUserManagerOrSuperuserOrSystem
from reportcreator_api.api_utils import backup_utils
from reportcreator_api.pentests.customfields.types import CweField
from reportcreator_api.users.models import AuthIdentity, PentestUser
from reportcreator_api.utils.api import StreamingHttpResponseAsync, ViewSetAsync
from reportcreator_api.utils import license
Expand Down Expand Up @@ -42,6 +43,7 @@ def list(self, *args, **kwargs):
return routers.APIRootView(api_root_dict={
'settings': 'utils-settings',
'license': 'utils-license',
'cwes': 'utils-cwes',
'spellcheck': 'utils-spellcheck',
'backup': 'utils-backup',
'healthcheck': 'utils-healthcheck',
Expand Down Expand Up @@ -134,6 +136,10 @@ async def spellcheck_add_word(self, request, *args, **kwargs):
data = await serializer.save()
return Response(data=data)

@action(detail=False, methods=['get'])
def cwes(self, request, *args, **kwargs):
return Response(data=CweField.cwe_definitions())

@action(detail=False, methods=['get'], authentication_classes=[], permission_classes=[])
async def healthcheck(self, request, *args, **kwargs):
# Trigger periodic tasks
Expand Down
5,606 changes: 5,606 additions & 0 deletions api/src/reportcreator_api/pentests/customfields/cwe.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@
}
}
},
{
"properties": {
"type": {
"const": "cwe"
},
"default": {
"type": ["string", "null"],
"pattern": "^CWE-[0-9]+$"
}
}
},
{
"properties": {
"type": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from reportcreator_api.pentests.customfields.types import CvssField, EnumChoice, EnumField, FieldOrigin, ListField, MarkdownField, ObjectField, StringField, DateField, UserField, BooleanField, field_definition_to_dict
from reportcreator_api.pentests.customfields.types import CvssField, CweField, EnumChoice, EnumField, FieldOrigin, ListField, MarkdownField, ObjectField, StringField, DateField, UserField, BooleanField, field_definition_to_dict
from reportcreator_api.utils.utils import copy_keys


Expand All @@ -20,6 +20,7 @@
items=StringField(origin=FieldOrigin.PREDEFINED, label='Reference', default=None)),
'affected_components': ListField(origin=FieldOrigin.PREDEFINED, label='Affected Components', required=True,
items=StringField(origin=FieldOrigin.PREDEFINED, label='Component', default='TODO: affected component')),
'cwe': CweField(origin=FieldOrigin.PREDEFINED, label='CWE', default=None, required=False),
'owasp_top10_2021': EnumField(origin=FieldOrigin.PREDEFINED, label='OWASP Top 10 - 2021', required=True, default=None, choices=[
EnumChoice(value='A01_2021', label='A01:2021 - Broken Access Control'),
EnumChoice(value='A02_2021', label='A02:2021 - Cryptographic Failures'),
Expand Down
16 changes: 13 additions & 3 deletions api/src/reportcreator_api/pentests/customfields/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes

from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition
from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition, CweField
from reportcreator_api.users.models import PentestUser


Expand All @@ -15,7 +15,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


class DateField(serializers.DateField):
class DateFieldSerializer(serializers.DateField):
def to_internal_value(self, value):
date = super().to_internal_value(value)
if isinstance(date, datetime.date):
Expand All @@ -24,6 +24,14 @@ def to_internal_value(self, value):
return date


class CweFieldSerializer(serializers.CharField):
def to_internal_value(self, data):
out = super().to_internal_value(data)
if not CweField.is_valid_cwe(out):
raise serializers.ValidationError('Not a valid CWE')
return out


class UserField(serializers.PrimaryKeyRelatedField):
queryset = PentestUser.objects.all()

Expand Down Expand Up @@ -60,13 +68,15 @@ def serializer_from_field(definition):
if field_type in [FieldDataType.STRING, FieldDataType.MARKDOWN, FieldDataType.CVSS, FieldDataType.COMBOBOX]:
return serializers.CharField(trim_whitespace=False, allow_blank=True, **value_field_kwargs)
elif field_type == FieldDataType.DATE:
return DateField(**value_field_kwargs)
return DateFieldSerializer(**value_field_kwargs)
elif field_type == FieldDataType.NUMBER:
return serializers.FloatField(**value_field_kwargs)
elif field_type == FieldDataType.BOOLEAN:
return serializers.BooleanField(**value_field_kwargs)
elif field_type == FieldDataType.ENUM:
return serializers.ChoiceField(choices=[c.value for c in definition.choices], **value_field_kwargs)
elif field_type == FieldDataType.CWE:
return CweFieldSerializer(**value_field_kwargs)
elif field_type == FieldDataType.USER:
return UserField(**value_field_kwargs)
elif field_type == FieldDataType.OBJECT:
Expand Down
4 changes: 4 additions & 0 deletions api/src/reportcreator_api/pentests/customfields/sort.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def format_sortable_field(value, definition):
return ''
elif definition.type == FieldDataType.CVSS:
return float(value['score'])
elif definition.type == FieldDataType.CWE:
if not value:
return -1
return int(value.replace('CWE-', ''))
elif definition.type == FieldDataType.ENUM:
# Sort enums by position of choice in choices list, not by value
return next((i for i, c in enumerate(definition.choices) if c.value == value['value']), -1)
Expand Down
24 changes: 24 additions & 0 deletions api/src/reportcreator_api/pentests/customfields/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import dataclasses
import enum
import functools
import json
from pathlib import Path
from frozendict import frozendict
from datetime import date
from inspect import isclass
Expand All @@ -16,6 +18,7 @@ class FieldDataType(enum.Enum):
STRING = 'string'
MARKDOWN = 'markdown'
CVSS = 'cvss'
CWE = 'cwe'
DATE = 'date'
NUMBER = 'number'
BOOLEAN = 'boolean'
Expand Down Expand Up @@ -120,6 +123,26 @@ def __post_init__(self):
'Default value is not a valid enum choice', self.default)


@deconstructible
@dataclasses.dataclass
class CweField(BaseStringField):
type: FieldDataType = FieldDataType.CWE

@staticmethod
@functools.cache
def cwe_definitions() -> list[dict]:
return json.loads((Path(__file__).parent / 'cwe.json').read_text())

@staticmethod
def is_valid_cwe(cwe):
return cwe is None or \
cwe in set(map(lambda c: f"CWE-{c['id']}", CweField.cwe_definitions()))

def __post_init__(self):
if not CweField.is_valid_cwe(self.default):
raise ValueError('Default value is not a valid CWE')


@deconstructible
@dataclasses.dataclass
class NumberField(FieldDefinition):
Expand Down Expand Up @@ -162,6 +185,7 @@ class ListField(FieldDefinition):
FieldDataType.STRING: StringField,
FieldDataType.MARKDOWN: MarkdownField,
FieldDataType.CVSS: CvssField,
FieldDataType.CWE: CweField,
FieldDataType.DATE: DateField,
FieldDataType.NUMBER: NumberField,
FieldDataType.BOOLEAN: BooleanField,
Expand Down
5 changes: 4 additions & 1 deletion api/src/reportcreator_api/pentests/customfields/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import dataclasses
import enum
import random
from types import NoneType
from lorem_text import lorem
from typing import Any, Iterable, Optional, Union
from django.utils import timezone

from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition, FieldOrigin
from reportcreator_api.pentests.customfields.types import CweField, FieldDataType, FieldDefinition, FieldOrigin
from reportcreator_api.utils.utils import is_date_string, is_uuid
from reportcreator_api.utils.error_messages import format_path

Expand Down Expand Up @@ -100,6 +101,8 @@ def ensure_defined_structure(value, definition: Union[dict[str, FieldDefinition]
return _default_or_demo_data(definition, 'n/a', handle_undefined=handle_undefined)
elif definition.type == FieldDataType.ENUM and not (isinstance(value, str) and value in {c.value for c in definition.choices}):
return _default_or_demo_data(definition, next(iter(map(lambda c: c.value, definition.choices)), None), handle_undefined=handle_undefined)
elif definition.type == FieldDataType.CWE and not (isinstance(value, (str, NoneType)) and not CweField.is_valid_cwe(value)):
return _default_or_demo_data(definition, 'CWE-89', handle_undefined=handle_undefined)
elif definition.type == FieldDataType.COMBOBOX and not isinstance(value, str):
return _default_or_demo_data(definition, next(iter(definition.suggestions), None), handle_undefined=handle_undefined)
elif definition.type == FieldDataType.DATE and not (isinstance(value, str) and is_date_string(value)):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible

from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition, parse_field_definition
from reportcreator_api.pentests.customfields.types import CweField, FieldDataType, FieldDefinition, parse_field_definition


@functools.cache
Expand Down Expand Up @@ -108,6 +108,8 @@ def compile_field(self, definition: FieldDataType):
return {'type': ['boolean', 'null']}
elif field_type == FieldDataType.ENUM:
return {'type': ['string', 'null'], 'enum': [c.value for c in definition.choices] + [None]}
elif field_type == FieldDataType.CWE:
return {'type': ['string', 'null'], 'enum': [f"CWE-{c['id']}" for c in CweField.cwe_definitions()] + [None]}
elif field_type == FieldDataType.USER:
return {'type': ['string', 'null'], 'format': 'uuid'}
elif field_type == FieldDataType.OBJECT:
Expand Down
10 changes: 7 additions & 3 deletions api/src/reportcreator_api/tasks/rendering/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from reportcreator_api.pentests.customfields.sort import sort_findings
from reportcreator_api.tasks.rendering import tasks
from reportcreator_api.pentests import cvss
from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition, EnumChoice
from reportcreator_api.pentests.customfields.types import CweField, FieldDataType, FieldDefinition, EnumChoice
from reportcreator_api.pentests.customfields.utils import HandleUndefinedFieldsOptions, ensure_defined_structure, iterate_fields
from reportcreator_api.users.models import PentestUser
from reportcreator_api.utils.error_messages import MessageLocationInfo, MessageLocationType
Expand Down Expand Up @@ -70,12 +70,16 @@ def format_template_field(value: Any, definition: FieldDefinition, members: Opti
return dataclasses.asdict(next(filter(lambda c: c.value == value, definition.choices), EnumChoice(value='', label='')))
elif value_type == FieldDataType.CVSS:
score_metrics = cvss.calculate_metrics(value)
return {
return score_metrics | {
'vector': value,
'score': str(round(score_metrics["final"]["score"], 2)),
'level': cvss.level_from_score(score_metrics["final"]["score"]).value,
'level_number': cvss.level_number_from_score(score_metrics["final"]["score"]),
**score_metrics
}
elif value_type == FieldDataType.CWE:
cwe_definition = next(filter(lambda c: value == f"CWE-{c['id']}", CweField.cwe_definitions()), {})
return cwe_definition | {
'value': value,
}
elif value_type == FieldDataType.USER:
return format_template_field_user(value, members=members)
Expand Down
1 change: 1 addition & 0 deletions api/src/reportcreator_api/tests/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def create_project_type(assets_kwargs=None, **kwargs) -> ProjectType:
'field_string': {'type': 'string', 'label': 'String Field', 'default': 'test'},
'field_markdown': {'type': 'markdown', 'label': 'Markdown Field', 'default': '# test\nmarkdown'},
'field_cvss': {'type': 'cvss', 'label': 'CVSS Field', 'default': 'n/a'},
'field_cwe': {'type': 'cwe', 'label': 'CWE Field', 'default': 'CWE-89'},
'field_date': {'type': 'date', 'label': 'Date Field', 'default': '2022-01-01'},
'field_int': {'type': 'number', 'label': 'Number Field', 'default': 10},
'field_bool': {'type': 'boolean', 'label': 'Boolean Field', 'default': False},
Expand Down
1 change: 1 addition & 0 deletions api/src/reportcreator_api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ def public_urls():
def guest_urls():
return [
('utils list', lambda s, c: c.get(reverse('utils-list'))),
('utils cwes', lambda s, c: c.get(reverse('utils-cwes'))),

*viewset_urls('pentestuser', get_kwargs=lambda s, detail: {'pk': 'self'}, retrieve=True, update=True, update_partial=True),
*viewset_urls('pentestuser', get_kwargs=lambda s, detail: {}, list=True),
Expand Down
6 changes: 6 additions & 0 deletions api/src/reportcreator_api/tests/test_customfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'field_string': {'type': 'string', 'label': 'String Field', 'default': 'test'},
'field_markdown': {'type': 'markdown', 'label': 'Markdown Field', 'default': '# test\nmarkdown'},
'field_cvss': {'type': 'cvss', 'label': 'CVSS Field', 'default': 'n/a'},
'field_cwe': {'type': 'cwe', 'label': 'CWE Field', 'default': 'CWE-89'},
'field_date': {'type': 'date', 'label': 'Date Field', 'default': '2022-01-01'},
'field_int': {'type': 'number', 'label': 'Number Field', 'default': 10},
'field_bool': {'type': 'boolean', 'label': 'Boolean Field', 'default': False},
Expand All @@ -52,6 +53,7 @@
(False, {'f': {'type': 'enum', 'label': 'Enum Field', 'choices': [{'value': 'v1'}]}}),
(False, {'f': {'type': 'enum', 'label': 'Enum Field', 'choices': [{'value': None}]}}),
(False, {'f': {'type': 'enum', 'label': 'Enum Field', 'choices': [{'label': 'Name only'}]}}),
(False, {'f': {'type': 'cwe', 'label': 'CWE Field', 'default': 'not a CWE'}}),
(False, {'f': {'type': 'combobox'}}),
(False, {'f': {'type': 'combobox', 'suggestions': [None]}}),
(False, {'f': {'type': 'object', 'label': 'Object Field'}}),
Expand All @@ -74,6 +76,7 @@ def test_definition_formats(valid, definition):
'field_string2': {'type': 'string', 'label': 'String Field', 'default': None},
'field_markdown': {'type': 'markdown', 'label': 'Markdown Field', 'default': '# test\nmarkdown'},
'field_cvss': {'type': 'cvss', 'label': 'CVSS Field', 'default': 'n/a'},
'field_cwe': {'type': 'cwe', 'label': 'CWE Field', 'default': None},
'field_date': {'type': 'date', 'label': 'Date Field', 'default': '2022-01-01'},
'field_int': {'type': 'number', 'label': 'Number Field', 'default': 10},
'field_bool': {'type': 'boolean', 'label': 'Boolean Field', 'default': False},
Expand All @@ -87,6 +90,7 @@ def test_definition_formats(valid, definition):
'field_string2': None,
'field_markdown': 'Some **markdown**\n* String\n*List',
'field_cvss': 'CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H',
'field_cwe': 'CWE-89',
'field_date': '2022-01-01',
'field_int': 17,
'field_bool': True,
Expand All @@ -99,6 +103,8 @@ def test_definition_formats(valid, definition):
}),
(False, {'f': {'type': 'string'}}, {'f': {}}),
(False, {'f': {'type': 'string'}}, {}),
(False, {'f': {'type': 'cwe'}}, {'f': 'not a CWE'}),
(False, {'f': {'type': 'cwe'}}, {'f': 'CWE-99999999'}),
(False, {'f': {'type': 'list', 'items': {'type': 'object', 'properties': {'f': {'type': 'string'}}}}}, {'f': [{'f': 'v'}, {'f': 1}]}),
(True, {'f': {'type': 'list', 'items': {'type': 'object', 'properties': {'f': {'type': 'string'}}}}}, {'f': [{'f': 'v'}, {'f': None}]}),
(True, {'f': {'type': 'list', 'items': {'type': 'string'}}}, {'f': []}),
Expand Down
1 change: 1 addition & 0 deletions api/src/reportcreator_api/tests/test_rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def extract_html_part(self, html, start=None, end=None):
('{{ report.field_enum.value }}', lambda self: self.project.data['field_enum']),
('{{ findings[0].cvss.vector }}', lambda self: self.finding.data['cvss']),
('{{ findings[0].cvss.score }}', lambda self: str(cvss.calculate_score(self.finding.data['cvss']))),
('{{ report.field_cwe.value }} {{ report.field_cwe.id }} {{ report.field_cwe.name }}', "CWE-89 89 Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')"),
('{{ data.pentesters[0].name }}', lambda self: self.project.imported_members[0]['name']),
('<template v-for="r in data.pentesters[0].roles">{{ r }}</template>', lambda self: ''.join(self.project.imported_members[0]['roles'])),
('{{ data.pentesters[1].name }}', lambda self: self.user.name),
Expand Down
37 changes: 37 additions & 0 deletions api/update_cwes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import io
import requests
import zipfile
import json
from pathlib import Path
from lxml import etree


def download_cwe_xml():
res = requests.get('https://cwe.mitre.org/data/xml/cwec_latest.xml.zip').content
with zipfile.ZipFile(file=io.BytesIO(res)) as zip:
return zip.read(zip.namelist()[0])


def main():
cwes = []
cwe_xml = etree.fromstring(download_cwe_xml())
weaknesses_xml = cwe_xml.findall('./Weaknesses/Weakness', namespaces=cwe_xml.nsmap)
for w in weaknesses_xml:
if w.attrib['Status'] == 'Deprecated':
continue
parent_ref = w.find('./Related_Weaknesses/Related_Weakness[@Nature="ChildOf"][@Ordinal="Primary"][@View_ID="1000"][@CWE_ID]', namespaces=cwe_xml.nsmap)
cwes.append({
'id': int(w.attrib['ID']),
'name': w.attrib['Name'],
'description': w.find('./Description', namespaces=cwe_xml.nsmap).text,
'parent': int(parent_ref.attrib['CWE_ID']) if parent_ref is not None else None,
})

cwes = sorted(cwes, key=lambda cwe: cwe['id'])
out_path = Path(__file__).parent / 'src/reportcreator_api/pentests/customfields/cwe.json'
out_path.write_text(json.dumps(cwes, indent=2))



if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions frontend/src/components/Design/InputFieldDefinition.vue
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ function updateType(type: FieldDataType) {
(type === FieldDataType.NUMBER && !(def instanceof Number)) ||
(type === FieldDataType.BOOLEAN && !(def instanceof Boolean)) ||
(type === FieldDataType.ENUM && !(newObj.choices || []).find(c => c.value === def)) ||
(type === FieldDataType.CWE && (!(def instanceof String) || !def.startsWith('CWE-'))) ||
(type === FieldDataType.DATE) ||
(type === FieldDataType.USER)
) {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/DynamicInputField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@
v-bind="fieldAttrs"
/>

<!-- CWE -->
<s-cwe-field
v-else-if="definition.type === 'cwe'"
v-model="formValue"
v-bind="fieldAttrs"
/>

<!-- User -->
<s-user-selection
v-else-if="definition.type === 'user'"
Expand Down
Loading

0 comments on commit c6b2848

Please sign in to comment.