Skip to content

Commit cb11fd8

Browse files
committed
Merge branch 'main' into CA-1383-add-a-placeholder-in-empty-lists-of-applied-controls-in-selectors
2 parents 6b12378 + b7a9583 commit cb11fd8

File tree

153 files changed

+7751
-1479
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+7751
-1479
lines changed

.github/workflows/rpm-build.yml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Build RPM Package
2+
3+
permissions:
4+
contents: write
5+
packages: read
6+
7+
on:
8+
push:
9+
tags:
10+
- "v*"
11+
workflow_dispatch:
12+
13+
# Cancel older in-progress runs for the same PR or the same ref (branch)
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
build-rpm:
20+
runs-on: ubuntu-24.04
21+
container:
22+
image: rockylinux:9
23+
options: --privileged
24+
25+
steps:
26+
- name: Install Git (required for checkout)
27+
run: |
28+
dnf install -y git
29+
30+
- name: Checkout Repository
31+
uses: actions/checkout@v4
32+
with:
33+
fetch-depth: 0
34+
35+
- name: Install Build Dependencies
36+
run: |
37+
dnf install -y \
38+
rpm-build \
39+
rpmdevtools \
40+
rsync \
41+
curl \
42+
wget \
43+
gcc \
44+
gcc-c++ \
45+
make \
46+
openssl-devel \
47+
bzip2-devel \
48+
libffi-devel \
49+
zlib-devel \
50+
readline-devel \
51+
sqlite-devel \
52+
xz-devel \
53+
tk-devel \
54+
ncurses-devel \
55+
gdbm-devel
56+
57+
- name: Install Node.js and pnpm
58+
run: |
59+
curl -fsSL https://rpm.nodesource.com/setup_22.x | bash -
60+
dnf install -y nodejs
61+
npm install -g pnpm
62+
63+
- name: Install Poetry
64+
run: |
65+
curl -sSL https://install.python-poetry.org | python3 -
66+
echo "/root/.local/bin" >> $GITHUB_PATH
67+
68+
- name: Generate Version
69+
id: version
70+
run: |
71+
VERSION=$(git describe --tags --always 2>/dev/null || echo 'dev')
72+
echo "version=$VERSION" >> $GITHUB_OUTPUT
73+
echo "Building version: $VERSION"
74+
75+
- name: Build RPM
76+
env:
77+
VERSION: ${{ steps.version.outputs.version }}
78+
run: |
79+
cd packaging/rhel
80+
bash build-rpm.sh
81+
82+
- name: Find Built RPM
83+
id: rpm
84+
run: |
85+
RPM_PATH=$(find packaging/rhel/RPMS -name "*.rpm" -type f | head -n 1)
86+
RPM_NAME=$(basename "$RPM_PATH")
87+
echo "path=$RPM_PATH" >> $GITHUB_OUTPUT
88+
echo "name=$RPM_NAME" >> $GITHUB_OUTPUT
89+
90+
# Get file size for summary
91+
RPM_SIZE=$(du -h "$RPM_PATH" | cut -f1)
92+
echo "size=$RPM_SIZE" >> $GITHUB_OUTPUT
93+
94+
echo "Built RPM: $RPM_NAME ($RPM_SIZE)"
95+
96+
- name: Upload RPM as Artifact
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: ciso-assistant-rpm-${{ steps.version.outputs.version }}
100+
path: ${{ steps.rpm.outputs.path }}
101+
retention-days: 90
102+
if-no-files-found: error
103+
104+
- name: Create Release and Upload RPM
105+
if: startsWith(github.ref, 'refs/tags/v')
106+
uses: softprops/action-gh-release@v2
107+
with:
108+
files: ${{ steps.rpm.outputs.path }}
109+
generate_release_notes: true
110+
draft: false
111+
prerelease: false
112+
env:
113+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114+
115+
- name: Job Summary
116+
run: |
117+
echo "## RPM Build Complete 🎉" >> $GITHUB_STEP_SUMMARY
118+
echo "" >> $GITHUB_STEP_SUMMARY
119+
echo "**Version:** \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
120+
echo "**Package:** \`${{ steps.rpm.outputs.name }}\`" >> $GITHUB_STEP_SUMMARY
121+
echo "**Size:** ${{ steps.rpm.outputs.size }}" >> $GITHUB_STEP_SUMMARY
122+
echo "" >> $GITHUB_STEP_SUMMARY
123+
echo "### Installation" >> $GITHUB_STEP_SUMMARY
124+
echo '```bash' >> $GITHUB_STEP_SUMMARY
125+
echo "sudo rpm -ivh ${{ steps.rpm.outputs.name }}" >> $GITHUB_STEP_SUMMARY
126+
echo '```' >> $GITHUB_STEP_SUMMARY

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,4 +628,4 @@ Unless otherwise noted, all files are © intuitem.
628628

629629
## Activity
630630

631-
![Alt](https://repobeats.axiom.co/api/embed/83162c6044da29efd7efa28f746b6bee5a3c6a8a.svg "Repobeats analytics image")
631+
![Alt](https://repobeats.axiom.co/api/embed/02f80d1b099ffd1ae66d9cfdc3a0e13234606f35.svg "Repobeats analytics image")

backend/ciso_assistant/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def set_ciso_assistant_url(_, __, event_dict):
200200
"library",
201201
"serdes",
202202
"integrations",
203+
"webhooks",
203204
"rest_framework",
204205
"knox",
205206
"drf_spectacular",
@@ -504,3 +505,7 @@ def set_ciso_assistant_url(_, __, event_dict):
504505

505506
AUDITLOG_RETENTION_DAYS = int(os.environ.get("AUDITLOG_RETENTION_DAYS", 90))
506507
AUDITLOG_MAX_RECORDS = int(os.environ.get("AUDITLOG_MAX_RECORDS", 50000))
508+
509+
WEBHOOK_ALLOW_PRIVATE_IPS = (
510+
os.environ.get("WEBHOOK_ALLOW_PRIVATE_IPS", "False") == "True"
511+
)

backend/ciso_assistant/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
# beware of the order of url patterns, this can change de behavior in case of multiple matches and avoid giving identical paths that could cause conflicts
2525
urlpatterns = [
26+
path("api/webhooks/", include("webhooks.urls")),
2627
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
2728
path(
2829
"api/schema/swagger/",

backend/core/apps.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class CoreConfig(AppConfig):
1111
verbose_name = "Core"
1212

1313
def ready(self):
14+
# This import runs the @webhook_registry.register decorator
15+
import core.webhooks
16+
1417
# avoid post_migrate handler if we are in the main, as it interferes with restore
1518
if not os.environ.get("RUN_MAIN"):
1619
post_migrate.connect(startup, sender=self)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 5.2.7 on 2025-11-28 08:36
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("core", "0116_validationflow_flowevent"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="evidencerevision",
15+
name="task_node",
16+
field=models.ForeignKey(
17+
blank=True,
18+
null=True,
19+
on_delete=django.db.models.deletion.SET_NULL,
20+
related_name="evidence_revisions",
21+
to="core.tasknode",
22+
verbose_name="Task Node",
23+
),
24+
),
25+
migrations.AddField(
26+
model_name="tasktemplate",
27+
name="evidences",
28+
field=models.ManyToManyField(
29+
blank=True,
30+
help_text="Evidences related to the task",
31+
related_name="task_templates",
32+
to="core.evidence",
33+
),
34+
),
35+
]

backend/core/models.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,6 +3388,14 @@ class EvidenceRevision(AbstractBaseModel, FolderMixin):
33883388
evidence = models.ForeignKey(
33893389
Evidence, on_delete=models.CASCADE, related_name="revisions"
33903390
)
3391+
task_node = models.ForeignKey(
3392+
"TaskNode",
3393+
on_delete=models.SET_NULL,
3394+
related_name="evidence_revisions",
3395+
blank=True,
3396+
null=True,
3397+
verbose_name=_("Task Node"),
3398+
)
33913399
version = models.IntegerField(
33923400
default=1,
33933401
verbose_name=_("version number"),
@@ -6637,6 +6645,12 @@ class TaskTemplate(NameDescriptionMixin, FolderMixin):
66376645
assigned_to = models.ManyToManyField(
66386646
User, verbose_name="Assigned to", blank=True, related_name="task_templates"
66396647
)
6648+
evidences = models.ManyToManyField(
6649+
Evidence,
6650+
blank=True,
6651+
help_text="Evidences related to the task",
6652+
related_name="task_templates",
6653+
)
66406654
assets = models.ManyToManyField(
66416655
Asset,
66426656
verbose_name="Related assets",
@@ -6774,10 +6788,17 @@ class TaskNode(AbstractBaseModel, FolderMixin):
67746788

67756789
to_delete = models.BooleanField(default=False)
67766790

6791+
def __str__(self):
6792+
return f"{self.task_template.name} ({self.due_date})"
6793+
67776794
@property
67786795
def assigned_to(self):
67796796
return self.task_template.assigned_to
67806797

6798+
@property
6799+
def expected_evidence(self):
6800+
return self.task_template.evidences.all()
6801+
67816802
@property
67826803
def assets(self):
67836804
return self.task_template.assets.all()

backend/core/serializers.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,7 @@ class EvidenceRevisionReadSerializer(BaseModelSerializer):
15521552
evidence = FieldsRelatedField()
15531553
folder = FieldsRelatedField()
15541554
str = serializers.CharField(source="__str__")
1555+
task_node = FieldsRelatedField()
15551556

15561557
class Meta:
15571558
model = EvidenceRevision
@@ -1569,6 +1570,9 @@ def create(self, validated_data):
15691570
models.Max("version")
15701571
)["version__max"]
15711572
validated_data["version"] = (max_version or 0) + 1
1573+
# Update evidence status to in_review when a new revision is submitted
1574+
evidence.status = Evidence.Status.IN_REVIEW
1575+
evidence.save()
15721576
return super().create(validated_data)
15731577

15741578

@@ -2399,6 +2403,7 @@ def get_timeline_entries(self, obj):
23992403
class TaskTemplateReadSerializer(BaseModelSerializer):
24002404
path = PathField(read_only=True)
24012405
folder = FieldsRelatedField()
2406+
evidences = FieldsRelatedField(many=True)
24022407
assets = FieldsRelatedField(many=True)
24032408
applied_controls = FieldsRelatedField(many=True)
24042409
compliance_assessments = FieldsRelatedField(many=True)
@@ -2417,7 +2422,6 @@ class TaskTemplateReadSerializer(BaseModelSerializer):
24172422
# Expose task_node fields directly
24182423
status = serializers.SerializerMethodField()
24192424
observation = serializers.SerializerMethodField()
2420-
evidences = serializers.SerializerMethodField()
24212425

24222426
class Meta:
24232427
model = TaskTemplate
@@ -2439,21 +2443,12 @@ def get_observation(self, obj):
24392443
task_node = self.get_task_node(obj)
24402444
return task_node.observation if task_node else ""
24412445

2442-
def get_evidences(self, obj):
2443-
task_node = self.get_task_node(obj)
2444-
if task_node:
2445-
return [{"id": e.id, "str": e.name} for e in task_node.evidences.all()]
2446-
return []
2447-
24482446

24492447
class TaskTemplateWriteSerializer(BaseModelSerializer):
24502448
status = serializers.CharField(required=False)
24512449
observation = serializers.CharField(
24522450
required=False, allow_blank=True, allow_null=True
24532451
)
2454-
evidences = serializers.PrimaryKeyRelatedField(
2455-
queryset=Evidence.objects.all(), many=True, required=False
2456-
)
24572452

24582453
class Meta:
24592454
model = TaskTemplate
@@ -2470,11 +2465,9 @@ def to_representation(self, instance):
24702465
if task_node:
24712466
data["status"] = task_node.status
24722467
data["observation"] = task_node.observation
2473-
data["evidences"] = [e.id for e in task_node.evidences.all()]
24742468
else:
24752469
data["status"] = None
24762470
data["observation"] = ""
2477-
data["evidences"] = []
24782471
return data
24792472

24802473
def create(self, validated_data):
@@ -2539,7 +2532,6 @@ def _extract_tasknode_fields(self, validated_data):
25392532
return {
25402533
"status": validated_data.pop("status", None),
25412534
"observation": validated_data.pop("observation", None),
2542-
"evidences": validated_data.pop("evidences", []),
25432535
}
25442536

25452537
def _sync_task_node(
@@ -2580,28 +2572,49 @@ def _sync_task_node(
25802572
task_node.observation = tasknode_data["observation"]
25812573
task_node.save()
25822574

2583-
evidences = tasknode_data.get("evidences")
2584-
if evidences is not None:
2585-
task_node.evidences.set(evidences)
2586-
25872575

25882576
class TaskNodeReadSerializer(BaseModelSerializer):
25892577
path = PathField(read_only=True)
2590-
task_template = FieldsRelatedField()
2578+
task_template = FieldsRelatedField(["folder", "id"])
25912579
folder = FieldsRelatedField()
25922580
name = serializers.SerializerMethodField()
25932581
assigned_to = FieldsRelatedField(many=True)
2594-
evidences = FieldsRelatedField(many=True)
2582+
evidences = FieldsRelatedField(["folder", "id"], many=True)
25952583
is_recurrent = serializers.BooleanField(source="task_template.is_recurrent")
2596-
applied_controls = FieldsRelatedField(many=True)
2597-
compliance_assessments = FieldsRelatedField(many=True)
2598-
assets = FieldsRelatedField(many=True)
2599-
risk_assessments = FieldsRelatedField(many=True)
2600-
findings_assessment = FieldsRelatedField(many=True)
2584+
expected_evidence = FieldsRelatedField(["folder", "id"], many=True)
2585+
evidence_reviewed = serializers.SerializerMethodField()
2586+
evidence_revisions_map = serializers.SerializerMethodField()
2587+
applied_controls = FieldsRelatedField(["folder", "id"], many=True)
2588+
compliance_assessments = FieldsRelatedField(["folder", "id"], many=True)
2589+
assets = FieldsRelatedField(["folder", "id"], many=True)
2590+
risk_assessments = FieldsRelatedField(["folder", "id"], many=True)
2591+
findings_assessment = FieldsRelatedField(["folder", "id"], many=True)
26012592

26022593
def get_name(self, obj):
26032594
return obj.task_template.name if obj.task_template else ""
26042595

2596+
def get_evidence_reviewed(self, obj):
2597+
evidence_reviewed = []
2598+
for evidence in obj.expected_evidence:
2599+
last_revision = evidence.last_revision
2600+
if last_revision and last_revision.task_node == obj:
2601+
evidence_reviewed.append(evidence.id)
2602+
return evidence_reviewed
2603+
2604+
def get_evidence_revisions_map(self, obj):
2605+
"""Returns a mapping of evidence ID to revision ID for this task node"""
2606+
from core.models import EvidenceRevision
2607+
2608+
evidence_revisions = {}
2609+
for evidence in obj.expected_evidence:
2610+
# Find revisions for this evidence that belong to this task node
2611+
revision = EvidenceRevision.objects.filter(
2612+
evidence=evidence, task_node=obj
2613+
).first()
2614+
if revision:
2615+
evidence_revisions[str(evidence.id)] = str(revision.id)
2616+
return evidence_revisions
2617+
26052618
class Meta:
26062619
model = TaskNode
26072620
exclude = ["to_delete"]
@@ -2610,7 +2623,7 @@ class Meta:
26102623
class TaskNodeWriteSerializer(BaseModelSerializer):
26112624
class Meta:
26122625
model = TaskNode
2613-
exclude = ["task_template"]
2626+
exclude = ["task_template", "evidences"]
26142627

26152628

26162629
class TerminologyReadSerializer(BaseModelSerializer):

0 commit comments

Comments
 (0)