Skip to content

Commit

Permalink
Merge branch 'intuitem:main' into pgssi-s
Browse files Browse the repository at this point in the history
  • Loading branch information
krismas authored May 9, 2024
2 parents ca7f268 + fce691f commit b79932f
Show file tree
Hide file tree
Showing 35 changed files with 10,489 additions and 87 deletions.
1 change: 0 additions & 1 deletion .github/workflows/backend-api-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
working-directory: ${{env.working-directory}}
run: |
touch .env
echo DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
echo DJANGO_DEBUG='True' >> .env
echo DB_HOST=localhost >> .env
echo EMAIL_HOST=localhost >> .env
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/backend-coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
image: postgres:14.1
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_PASSWORD: postgres # test credential
POSTGRES_DB: postgres
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Expand All @@ -46,11 +46,10 @@ jobs:
working-directory: ${{env.working-directory}}
run: |
touch .env
echo DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
echo DJANGO_DEBUG='True' >> .env
echo POSTGRES_NAME=postgres >> .env
echo POSTGRES_USER=postgres >> .env
echo POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} >> .env
echo POSTGRES_PASSWORD=postgres >> .env
echo DB_HOST=localhost >> .env
echo EMAIL_HOST=localhost >> .env
echo EMAIL_PORT=1025 >> .env
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/functional-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
image: postgres:14.1
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_PASSWORD: postgres # test credential
POSTGRES_DB: postgres
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Expand Down Expand Up @@ -68,12 +68,11 @@ jobs:
working-directory: ${{ env.backend-directory }}
run: |
touch .env
echo DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
echo [email protected] >> .env
echo DJANGO_SUPERUSER_PASSWORD=1234 >> .env
echo POSTGRES_NAME=postgres >> .env
echo POSTGRES_USER=postgres >> .env
echo POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} >> .env
echo POSTGRES_PASSWORD=postgres >> .env
echo DB_HOST=localhost >> .env
echo CISO_ASSISTANT_SUPERUSER_EMAIL='' >> .env
echo CISO_ASSISTANT_URL=http://localhost:4173 >> .env
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/startup-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ jobs:
working-directory: ${{ env.backend-directory }}
run: |
touch .env
echo DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
echo [email protected] >> .env
echo DJANGO_SUPERUSER_PASSWORD=1234 >> .env
echo POSTGRES_NAME=postgres >> .env
Expand Down Expand Up @@ -114,7 +113,6 @@ jobs:
working-directory: ${{ env.backend-directory }}
run: |
touch .env
echo DJANGO_SECRET_KEY=${{ secrets.DJANGO_SECRET_KEY }} >> .env
export $(grep -v '^#' .env | xargs)
- name: Config the Docker app
run: |
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ Check out the online documentation on https://intuitem.gitbook.io/ciso-assistant
18. HDS/HDH 🇫🇷
19. OWASP Application Security Verification Standard (ASVS) 🐝
20. RGS v2.0 🇫🇷
21. AirCyber ✈️
21. AirCyber ✈️🌐
22. Cyber Resilience Act (CRA) 🇪🇺
23. TIBER-EU 🇪🇺
24. NIST Privacy Framework 🇺🇸
25. Tisax 🚘
25. TISAX (VDA ISA) 🚘
26. ANSSI hygiene guide 🇫🇷
27. Essential Cybersecurity Controls (ECC) 🇸🇦
28. CIS Controls v8\*
Expand All @@ -103,6 +103,8 @@ Check out the online documentation on https://intuitem.gitbook.io/ciso-assistant
31. NIST SP 800-171 rev2 🇺🇸
32. ANSSI : recommandations de sécurité pour un système d'IA générative 🇫🇷🤖
33. NIST SP 800-218: Secure Software Development Framework (SSDF) 🖥️
34. GSA FedRAMP rev5 ☁️🇺🇸
35. Cadre Conformité Cyber France (3CF) ✈️🇫🇷

<br/>

Expand All @@ -116,13 +118,13 @@ Checkout the [library](/backend/library/libraries/) and [tools](/tools/) for the
### Coming soon

- FBI CJIS
- BSI-IT (as requested by the German community)
- CCPA
- AI Act
- Part-IS
- SecNumCloud
- SOX
- MASVS
- FedRAMP
- NIST 800-82
- NCSC Cyber Assessment Framework (CAF)
- UK Cyber Essentials
Expand Down
3 changes: 3 additions & 0 deletions backend/app_tests/api/test_api_compliance_assessments.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def test_get_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": 1,
"max_score": 4,
},
Expand Down Expand Up @@ -157,6 +158,7 @@ def test_create_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": Framework.objects.all()[0].min_score,
"max_score": Framework.objects.all()[0].max_score,
},
Expand Down Expand Up @@ -202,6 +204,7 @@ def test_update_compliance_assessments(self, test):
"framework": {
"id": str(Framework.objects.all()[0].id),
"str": str(Framework.objects.all()[0]),
"implementation_groups_definition": None,
"min_score": Framework.objects.all()[0].min_score,
"max_score": Framework.objects.all()[0].max_score,
},
Expand Down
47 changes: 47 additions & 0 deletions backend/core/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections.abc import MutableMapping
from datetime import date, timedelta

from django.db.models import Count
Expand All @@ -9,6 +10,20 @@
from .models import *
from .utils import camel_case


def flatten_dict(
d: MutableMapping, parent_key: str = "", sep: str = "."
) -> MutableMapping:
items = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, MutableMapping):
items.extend(flatten_dict(v, new_key, sep=sep).items())
else:
items.append((new_key, v))
return dict(items)


STATUS_COLOR_MAP = { # TODO: Move these kinds of color maps to frontend
"undefined": "#CCC",
"planned": "#BFDBFE",
Expand Down Expand Up @@ -240,6 +255,9 @@ def get_sorted_requirement_nodes_rec(
"parent_urn": node.parent_urn,
"ref_id": node.ref_id,
"name": node.name,
"implementation_groups": node.implementation_groups
if node.implementation_groups
else None,
"ra_id": str(req_as.id) if requirements_assessed else None,
"status": req_as.status if requirements_assessed else None,
"is_scored": req_as.is_scored if requirements_assessed else None,
Expand Down Expand Up @@ -275,6 +293,9 @@ def get_sorted_requirement_nodes_rec(
{
"urn": req.urn,
"ref_id": req.ref_id,
"implementation_groups": req.implementation_groups
if req.implementation_groups
else None,
"name": req.name,
"description": req.description,
"ra_id": str(req_as.id),
Expand Down Expand Up @@ -322,6 +343,32 @@ def get_sorted_requirement_nodes_rec(
return tree


def filter_graph_by_implementation_groups(
graph: dict[str, dict], implementation_groups: set[str] | None
) -> dict[str, dict]:
if not implementation_groups:
return graph

def should_include_node(node: dict) -> bool:
node_groups = node.get("implementation_groups")
if node_groups:
return any(group in node_groups for group in implementation_groups)

# Nodes without implementation groups but with children are included
return bool(node.get("children"))

filtered_graph = {}
for key, value in graph.items():
if value.get("children"):
value["children"] = filter_graph_by_implementation_groups(
value["children"], implementation_groups
)
if should_include_node(value):
filtered_graph[key] = value

return filtered_graph


def get_parsed_matrices(user: User, risk_assessments: list | None = None):
(
object_ids_view,
Expand Down
81 changes: 60 additions & 21 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,10 +1290,34 @@ def get_global_score(self):
.exclude(status=RequirementAssessment.Status.NOT_APPLICABLE)
.exclude(is_scored=False)
)
score = requirement_assessments_scored.aggregate(models.Avg("score"))
if score["score__avg"] is not None:
return round(score["score__avg"], 1)
return -1
ig = (
set(self.selected_implementation_groups)
if self.selected_implementation_groups
else None
)
score = 0
n = 0
for ras in requirement_assessments_scored:
if not (ig) or (ig & set(ras.requirement.implementation_groups)):
score += ras.score
n += 1
if n > 0:
return round(score / n, 1)
else:
return -1

def get_selected_implementation_groups(self):
framework = self.framework
if (
not framework.implementation_groups_definition
or not self.selected_implementation_groups
):
return []
return [
group.get("name")
for group in framework.implementation_groups_definition
if group.get("ref_id") in self.selected_implementation_groups
]

def get_requirements_status_count(self):
requirements_status_count = []
Expand Down Expand Up @@ -1332,7 +1356,13 @@ def get_measures_status_count(self):
return measures_status_count

def donut_render(self) -> dict:
compliance_assessments_status = {"values": [], "labels": []}
def union_queries(base_query, groups, field_name):
queries = [
base_query.filter(**{f"{field_name}__icontains": group}).distinct()
for group in groups
]
return queries[0].union(*queries[1:]) if queries else base_query.none()

color_map = {
"in_progress": "#3b82f6",
"non_compliant": "#f87171",
Expand All @@ -1341,24 +1371,33 @@ def donut_render(self) -> dict:
"not_applicable": "#000000",
"compliant": "#86efac",
}
for st in RequirementAssessment.Status:
count = (
RequirementAssessment.objects.filter(status=st)
.filter(compliance_assessment=self)
.filter(requirement__assessable=True)
.count()
)
total = RequirementAssessment.objects.filter(
compliance_assessment=self
).count()
v = {
"name": st,
"localName": camel_case(st.value),

compliance_assessments_status = {"values": [], "labels": []}
for status in RequirementAssessment.Status:
base_query = RequirementAssessment.objects.filter(
status=status, compliance_assessment=self, requirement__assessable=True
).distinct()

if self.selected_implementation_groups:
union_query = union_queries(
base_query,
self.selected_implementation_groups,
"requirement__implementation_groups",
)
else:
union_query = base_query

count = union_query.count()
value_entry = {
"name": status,
"localName": camel_case(status.value),
"value": count,
"itemStyle": {"color": color_map[st]},
"itemStyle": {"color": color_map[status]},
}
compliance_assessments_status["values"].append(v)
compliance_assessments_status["labels"].append(st.label)

compliance_assessments_status["values"].append(value_entry)
compliance_assessments_status["labels"].append(status.label)

return compliance_assessments_status

def quality_check(self) -> dict:
Expand Down
9 changes: 8 additions & 1 deletion backend/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@ def has_object_permission(self, request: Request, view, obj):
_codename = perms[0].split(".")[1]
if request.method in ["GET", "OPTIONS", "HEAD"] and obj.is_published:
return True
perm = Permission.objects.get(codename=_codename)
# special case of risk acceptance approval
if (
request.parser_context["request"]._request.resolver_match.url_name
== "risk-acceptances-accept"
):
perm = Permission.objects.get(codename="approve_riskacceptance")
return RoleAssignment.is_access_allowed(
user=request.user,
perm=Permission.objects.get(codename=_codename),
perm=perm,
folder=Folder.get_folder(obj),
)

Expand Down
7 changes: 6 additions & 1 deletion backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,12 @@ class Meta:


class ComplianceAssessmentReadSerializer(AssessmentReadSerializer):
framework = FieldsRelatedField(["id", "min_score", "max_score"])
framework = FieldsRelatedField(
["id", "min_score", "max_score", "implementation_groups_definition"]
)
selected_implementation_groups = serializers.ReadOnlyField(
source="get_selected_implementation_groups"
)

class Meta:
model = ComplianceAssessment
Expand Down
Loading

0 comments on commit b79932f

Please sign in to comment.