Skip to content

Commit b7d20f5

Browse files
authored
implement pdf conversion [WIP] (#271)
* implement pdf conversion #267 * fix test failure? * implement processing-log #270 and attack-navigator #269 * add missing migration
1 parent 6d69722 commit b7d20f5

19 files changed

Lines changed: 680 additions & 81 deletions

.github/workflows/run-tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,16 @@ jobs:
4848
export DJANGO_WWW_PATH=$PWD/django_www_path/
4949
echo DJANGO_WWW_PATH="$DJANGO_WWW_PATH" >> tests/tests.env
5050
mkdir -p $DJANGO_WWW_PATH
51+
5152
pip install -r tests/requirements.txt -r requirements.txt
53+
sudo bash ./install_deps.sh
54+
5255
5356
- name: Unit tests
5457
id: unit_tests
5558
run: |
5659
set -a; source tests/tests.env; set +a
57-
pytest --cov --cov-branch --cov-report=xml --junitxml=unittest.junit.xml -o junit_family=legacy tests/ --ignore=tests/st/
60+
pytest --cov --cov-branch --cov-report=xml --cov-report=term --junitxml=unittest.junit.xml -o junit_family=legacy tests/ --ignore=tests/st/
5861
5962
- name: Schema tests
6063
id: schema_tests

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
FROM python:3.12-slim
22
ENV PYTHONUNBUFFERED=1
33
WORKDIR /usr/src/app
4+
5+
COPY install_deps.sh .
6+
RUN --mount=type=cache,target=/var/cache/apt/archives/ \
7+
bash install_deps.sh
8+
9+
sudo bash ./install_deps.sh
410
COPY requirements.txt ./
511
RUN --mount=type=cache,target=/root/.cache \
612
pip install -r requirements.txt

Dockerfile.deploy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ ENV R2_CUSTOM_DOMAIN=""
2929
ENV SRO_OBJECTS_ONLY_LATEST=False
3030

3131
WORKDIR /usr/src/app
32+
COPY install_deps.sh .
33+
RUN --mount=type=cache,target=/var/cache/apt/archives/ \
34+
bash install_deps.sh
35+
3236
COPY requirements.txt ./
3337
RUN --mount=type=cache,target=/root/.cache \
3438
pip install -r requirements.txt

install_deps.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
apt update
2+
apt install -y libreoffice
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 5.2.4 on 2025-10-09 14:33
2+
3+
import siemrules.siemrules.models
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('siemrules', '0020_alter_job_type'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='file',
16+
name='pdf_file',
17+
field=models.FileField(max_length=1024, null=True, upload_to=siemrules.siemrules.models.upload_to_func),
18+
),
19+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.4 on 2025-10-10 12:04
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('siemrules', '0021_file_pdf_file'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='file',
15+
name='txt2detection_data',
16+
field=models.JSONField(default=None, null=True),
17+
),
18+
]

siemrules/siemrules/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class File(models.Model):
5959
extract_text_from_image = models.BooleanField(default=True)
6060
ai_provider = models.CharField(max_length=256, null=True)
6161
markdown_file = models.FileField(max_length=512, upload_to=upload_to_func, null=True)
62+
pdf_file = models.FileField(max_length=1024, upload_to=upload_to_func, null=True)
63+
txt2detection_data = models.JSONField(default=None, null=True)
6264

6365
created = models.DateTimeField(default=timezone.now, null=True)
6466

@@ -87,6 +89,13 @@ def clean(self) -> None:
8789

8890
def __str__(self) -> str:
8991
return f"File(id={self.id})"
92+
93+
94+
@property
95+
def archived_pdf(self):
96+
if self.mode == 'pdf':
97+
return self.file
98+
return self.pdf_file
9099

91100
@receiver(post_delete, sender=File)
92101
def remove_reports_on_delete(sender, instance: File, **kwargs):

siemrules/siemrules/serializers.py

Lines changed: 97 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.template.defaultfilters import slugify
1010
import stix2, json
1111
from txt2detection.models import TAG_PATTERN
12+
from dogesec_commons.utils.serializers import JSONSchemaSerializer
1213

1314

1415
def validate_model(model):
@@ -86,8 +87,8 @@ def __init__(self, *args, **kwargs):
8687
super().__init__(*args, **kwargs)
8788

8889
def to_internal_value(self, data):
89-
if isinstance(data, (str, Mapping)) or not hasattr(data, '__iter__'):
90-
self.fail('not_a_list', input_type=type(data).__name__)
90+
if isinstance(data, (str, Mapping)) or not hasattr(data, "__iter__"):
91+
self.fail("not_a_list", input_type=type(data).__name__)
9192
retval = []
9293
if hasattr(self.parent, "skip_csv"):
9394
retval = data
@@ -110,7 +111,7 @@ class FileSerializer(serializers.ModelSerializer):
110111
help_text="Full path to the file to be converted. The mimetype of the file uploaded must match that expected by the `mode` selected. This is a file2txt setting.",
111112
)
112113
mode = serializers.ChoiceField(
113-
choices=list(f2t_core.BaseParser.PARSERS.keys()),
114+
choices=list(["txt", "html", "html_article", "word", "pdf", "md"]),
114115
help_text="How the File should be processed. This is a file2txt setting.",
115116
)
116117
report_id = ReportIDField(
@@ -181,6 +182,7 @@ class Meta:
181182
exclude = ["markdown_file", "status", "level"]
182183
read_only_fields = ["id"]
183184

185+
184186
class FileDocumentSerializer(FileSerializer):
185187
type_label = "siemrules.file"
186188
ai_provider = serializers.CharField(
@@ -305,8 +307,6 @@ class RuleCloneSerializer(serializers.Serializer):
305307
description = serializers.CharField(required=False)
306308

307309

308-
309-
310310
class HealthCheckChoices(StrEnum):
311311
AUTHORIZED = auto()
312312
UNAUTHORIZED = auto()
@@ -315,19 +315,109 @@ class HealthCheckChoices(StrEnum):
315315
UNKNOWN = auto()
316316
OFFLINE = auto()
317317

318+
318319
class HealthCheckChoiceField(serializers.ChoiceField):
319320
def __init__(self, **kwargs):
320321
choices = [m.value for m in HealthCheckChoices]
321322
super().__init__(choices, **kwargs)
322-
323+
324+
323325
class HealthCheckLLMs(serializers.Serializer):
324326
openai = HealthCheckChoiceField()
325327
deepseek = HealthCheckChoiceField()
326328
anthropic = HealthCheckChoiceField()
327329
gemini = HealthCheckChoiceField()
328330
openrouter = HealthCheckChoiceField()
329331

332+
330333
class HealthCheckSerializer(serializers.Serializer):
331334
ctibutler = HealthCheckChoiceField()
332335
vulmatch = HealthCheckChoiceField()
333-
llms = HealthCheckLLMs()
336+
llms = HealthCheckLLMs()
337+
338+
339+
class AttackNavigatorDomainSerializer(JSONSchemaSerializer):
340+
json_schema = {
341+
"$schema": "http://json-schema.org/draft-07/schema#",
342+
"title": "MITRE ATT&CK Navigator Layer v4.5",
343+
"type": "object",
344+
"required": ["versions", "name", "domain", "techniques"],
345+
"properties": {
346+
"versions": {
347+
"type": "object",
348+
"properties": {
349+
"layer": {"type": "string", "example": "4.5"},
350+
"attack": {"type": "string", "example": "17.0"},
351+
"navigator": {"type": "string", "example": "5.1.0"},
352+
},
353+
"required": ["layer", "attack", "navigator"],
354+
"additionalProperties": False,
355+
},
356+
"name": {"type": "string"},
357+
"domain": {
358+
"type": "string",
359+
"enum": ["enterprise-attack", "mobile-attack", "ics-attack"],
360+
},
361+
"description": {"type": "string"},
362+
"gradient": {
363+
"type": "object",
364+
"required": ["colors", "minValue", "maxValue"],
365+
"properties": {
366+
"colors": {
367+
"type": "array",
368+
"items": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
369+
},
370+
"minValue": {"type": "number"},
371+
"maxValue": {"type": "number"},
372+
},
373+
},
374+
"legendItems": {
375+
"type": "array",
376+
"items": {
377+
"type": "object",
378+
"properties": {
379+
"label": {"type": "string"},
380+
"color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
381+
"value": {"type": "number"},
382+
},
383+
},
384+
},
385+
"showTacticsRowBackground": {"type": "boolean"},
386+
"techniques": {
387+
"type": "array",
388+
"items": {
389+
"type": "object",
390+
"required": ["techniqueID"],
391+
"properties": {
392+
"techniqueID": {"type": "string"},
393+
"tactic": {"type": "string"},
394+
"score": {"type": ["number", "null"]},
395+
"color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
396+
"comment": {"type": "string"},
397+
"enabled": {"type": "boolean"},
398+
"links": {
399+
"type": "array",
400+
"items": {
401+
"type": "object",
402+
"properties": {
403+
"href": {"type": "string", "format": "uri"},
404+
"text": {"type": "string"},
405+
},
406+
"required": ["href", "text"],
407+
},
408+
},
409+
},
410+
"additionalProperties": True,
411+
},
412+
},
413+
"tacticUseIds": {"type": "array", "items": {"type": "string"}},
414+
"filters": {
415+
"type": "object",
416+
"properties": {
417+
"includeSubtechniques": {"type": "boolean"},
418+
"showOnlyVisibleTechniques": {"type": "boolean"},
419+
},
420+
},
421+
},
422+
"additionalProperties": True,
423+
}

siemrules/siemrules/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,13 @@ def parse(self, stream, media_type=None, parser_context=None):
4343
return yaml.safe_load(stream)
4444
except Exception as e:
4545
raise ParseError(f"parse yaml error: {e}") from e
46+
47+
48+
class PDFRenderer(renderers.BaseRenderer):
49+
media_type = 'application/pdf'
50+
format = 'pdf'
51+
charset = None
52+
render_style = 'binary'
53+
54+
def render(self, data, media_type=None, renderer_context=None):
55+
return data # You must return raw bytes here (PDF content)

0 commit comments

Comments
 (0)