Skip to content

Commit

Permalink
Merge branch 'intuitem:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
protocolpaladin authored May 9, 2024
2 parents 59802da + 944484d commit 2b09fb6
Show file tree
Hide file tree
Showing 48 changed files with 11,741 additions and 161 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
8 changes: 3 additions & 5 deletions .github/workflows/startup-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ env:
GITHUB_WORKFLOW: github_actions
backend-directory: ./backend
working-directory: ./frontend

jobs:
startup-functional-test:
runs-on: ubuntu-20.04
Expand All @@ -18,7 +18,7 @@ jobs:
image: postgres:14.1
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports: ["5432:5432"]
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
Expand Down Expand Up @@ -60,12 +60,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 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
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,21 @@ 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\*
29. CSA CCM (Cloud Controls Matrix)\*
30. FADP (Federal Act on Data Protection) 🇨🇭
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 @@ -115,22 +118,21 @@ 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
- NIST SP 800-218
- SOX
- MASVS
- FedRAMP
- NIST 800-82
- NCSC Cyber Assessment Framework (CAF)
- UK Cyber Essentials
- and much more: just ask on [Discord](https://discord.gg/qvkaMdQ8da). If it's an open standard, we'll do it for you, _free of charge_ 😉

### Add your own framework
### Add your own library (framework, threat catalog, reference controls catalog or matrix)

Have a look in the tools directory and its dedicated readme. The convert_framework.py script will help you create your library from a simple Excel file. A typical framework can be ingested in a few hours.
Have a look in the tools directory and its dedicated readme. The convert_library.py script will help you create your library from a simple Excel file. A typical framework can be ingested in a few hours.

You will also find some specific converters in the tools directory (e.g. for CIS or CCM Controls).

Expand Down Expand Up @@ -178,6 +180,10 @@ For the following executions, use "docker compose up" directly.
> [!TIP]
> If you want a fresh install, simply delete the `db` directory, (default: backend/db) where the database is stored.
## Docker-compose on remote

For docker setup on a remote server or hypervisor, checkout the [specific instructions here](https://intuitem.gitbook.io/ciso-assistant/deployment/remote-virtualization)

## Setting up CISO Assistant for development

### Requirements
Expand Down Expand Up @@ -401,6 +407,7 @@ Set DJANGO_DEBUG=False for security reason.
- [Django](https://www.djangoproject.com/) - Python Web Development Framework
- [SvelteKit](https://kit.svelte.dev/) - Frontend framework
- [Gunicorn](https://gunicorn.org/) - Python WSGI HTTP Server for UNIX
- [Caddy](https://caddyserver.com) - The coolest reverse Proxy
- [Gitbook](https://www.gitbook.com) - Documentation platform
- [PostgreSQL](https://www.postgresql.org/) - Open Source RDBMS
- [SQLite](https://www.sqlite.org/index.html) - Open Source RDBMS
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
4 changes: 4 additions & 0 deletions backend/ciso_assistant/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ def set_ciso_assistant_url(_, __, event_dict):
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"].append(
"rest_framework.renderers.BrowsableAPIRenderer"
)
# Add session authentication to allow using the browsable API
REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append(
"rest_framework.authentication.SessionAuthentication"
)

INSTALLED_APPS.append("django.contrib.staticfiles")
STATIC_URL = "/static/"
Expand Down
48 changes: 48 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 Expand Up @@ -479,6 +526,7 @@ def applied_control_per_status(user: User):
v = {"value": count, "itemStyle": {"color": color_map[st[0]]}}
values.append(v)
labels.append(st[1])
labels.insert(0, "undefined")
local_lables = [camel_case(str(label)) for label in labels]
return {"localLables": local_lables, "labels": labels, "values": values}

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
Loading

0 comments on commit 2b09fb6

Please sign in to comment.