Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/cicd-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ jobs:
uses: actions/checkout@v4

- name: Create .env file
env:
POSTGRES_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }}
SALT: ${{ secrets.TEST_SALT }}
run: |
echo "NVD_API_KEY=${{ secrets.TEST_NVD_API_KEY }}" >> .env
echo 'DJANGO_SECRET_KEY="${{ secrets.TEST_DJANGO_SECRET_KEY }}"' >> .env
echo 'SALT="${{ secrets.TEST_SALT }}"' >> .env
echo "SALT=${SALT:-local-dev-salt}" >> .env
echo "ADMIN_USERNAME=admin@acme.de" >> .env
echo "ADMIN_PASSWORD=secure!" >> .env
echo "USER_USERNAME=user@acme.de" >> .env
Expand All @@ -41,7 +44,7 @@ jobs:
echo "POSTGRES_USER=securecheckplus" >> .env
echo "POSTGRES_DB=securecheckplus" >> .env
echo "POSTGRES_PORT=5432" >> .env
echo 'POSTGRES_PASSWORD="${{ secrets.TEST_DB_PASSWORD }}"' >> .env
echo "POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-scp_test_pass}" >> .env
echo "EMAIL_HOST=localhost" >> .env
echo "EMAIL_PORT=25" >> .env
echo 'LDAP_ORGANISATION="ACME"' >> .env
Expand Down Expand Up @@ -77,6 +80,7 @@ jobs:
path: backend

- name: Docker Login
if: env.DOCKER_USER != '' && env.DOCKER_KEY != ''
run: echo "$DOCKER_KEY" | docker login -u "$DOCKER_USER" --password-stdin

- name: Build Docker Compose
Expand Down Expand Up @@ -182,6 +186,7 @@ jobs:
path: backend/assets

- name: Docker Login
if: env.DOCKER_USER != '' && env.DOCKER_KEY != ''
run: echo "$DOCKER_KEY" | docker login -u "$DOCKER_USER" --password-stdin

- name: Extract metadata (tags, labels) for Docker
Expand Down Expand Up @@ -221,6 +226,7 @@ jobs:
path: backend

- name: Docker Login
if: env.DOCKER_USER != '' && env.DOCKER_KEY != ''
run: echo "$DOCKER_KEY" | docker login -u "$DOCKER_USER" --password-stdin

- name: Extract metadata (tags, labels) for Docker
Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ RUN apk update --no-cache \

ENTRYPOINT ["sh", "/entrypoint.sh"]

CMD python manage.py runserver 0.0.0.0:8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

FROM dev as prod
#ARG is required for the settings.py. Otherwise the build will fail, because required env variables have not been set
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.1.2 on 2025-02-10 15:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('analyzer', '0002_initial'),
]

operations = [
migrations.AlterField(
model_name='dependency',
name='package_manager',
field=models.CharField(default='NA', max_length=255),
),
migrations.AlterField(
model_name='project',
name='project_id',
field=models.CharField(max_length=50, unique=True),
),
migrations.AlterField(
model_name='project',
name='project_name',
field=models.CharField(blank=True, max_length=50),
),
migrations.AlterField(
model_name='report',
name='overall_cvss_severity',
field=models.CharField(max_length=255, null=True),
),
]
4 changes: 2 additions & 2 deletions backend/analyzer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class Project(models.Model):
class Meta:
db_table = constants.DB_SCHEMA_PREFIX + "project"

project_id = models.CharField(max_length=25, blank=False, unique=True)
project_name = models.CharField(max_length=25, blank=True)
project_id = models.CharField(max_length=50, blank=False, unique=True)
project_name = models.CharField(max_length=50, blank=True)
updated = models.DateTimeField(auto_now=True)
deployment_threshold = models.CharField(max_length=20,
choices=constants.Threshold.choices,
Expand Down
65 changes: 65 additions & 0 deletions backend/analyzer/test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from unittest.mock import patch

import pytest

MOCK_NVD_RESPONSE = {
"vulnerabilities": [
{
"cve": {
"id": "CVE-2021-44228",
"published": "2025-01-01T00:00:00Z",
"lastModified": "2025-01-01T00:00:00Z",
"descriptions": [{"value": "A test vulnerability description."}],
"metrics": {
"cvssMetricV31": [
{
"cvssData": {
"baseScore": 7.5,
"baseSeverity": "HIGH",
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"confidentialityImpact": "HIGH",
"integrityImpact": "NONE",
"availabilityImpact": "NONE",
"scope": "UNCHANGED",
}
}
]
},
"weaknesses": [{"description": [{"value": "CWE-94"}]}],
"references": [{"url": "https://example.com/advisory", "tags": ["Vendor Advisory"]}],
}
}
]
}

MOCK_EPSS_RESPONSE = {"data": [{"epss": 0.5}]}


def _mock_requests_get(url, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code

def json(self):
return self.json_data

url_str = str(url)
if "nvd.nist.gov" in url_str:
return MockResponse(MOCK_NVD_RESPONSE, 200)
if "api.first.org" in url_str:
return MockResponse(MOCK_EPSS_RESPONSE, 200)
raise ConnectionError(f"Unexpected request: {url_str}")


@pytest.fixture(autouse=True)
def no_nvd_network(request):
if request.node.get_closest_marker("nvd_integration"):
yield
return
with patch("analyzer.services.cve_fetcher.requests.get", side_effect=_mock_requests_get), \
patch("analyzer.services.cve_fetcher.time.sleep"):
yield
31 changes: 22 additions & 9 deletions backend/analyzer/test/test_cve_fetcher.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
from datetime import datetime

import pytest

from analyzer.services.cve_fetcher import CVEFetcher
from utilities.constants import BaseSeverity, AttackVector, AttackComplexity, UserInteraction, IntegrityImpact, \
AvailabilityImpact, ConfidentialityImpact, Scope, PrivilegesRequired

cve_id = "CVE-2021-44228"
cve_fetcher = CVEFetcher(cve_id=cve_id)
cve_data = cve_fetcher.generate()


def test_description():
@pytest.fixture
def cve_data():
fetcher = CVEFetcher(cve_id=cve_id)
return fetcher.generate()


@pytest.mark.nvd_integration
def test_real_nvd_api_contract():
fetcher = CVEFetcher(cve_id=cve_id)
data = fetcher.generate()
assert fetcher.successful
assert len(data["description"]) > 0
assert 0 < data["cve_attributes"]["baseScore"] <= 10


def test_description(cve_data):
assert len(cve_data["description"]) > 0


def test_dates():
def test_dates(cve_data):
published = cve_data["published"]
assert isinstance(published, datetime)

updated = cve_data["updated"]
assert isinstance(updated, datetime)


def test_cve_attributes_cvss_v3():
def test_cve_attributes_cvss_v3(cve_data):
attributes = cve_data["cve_attributes"]

assert 0 < attributes["baseScore"] <= 10
assert attributes["baseSeverity"] in BaseSeverity.names
assert attributes["attackVector"] in AttackVector.names
Expand All @@ -36,9 +49,9 @@ def test_cve_attributes_cvss_v3():
assert attributes["scope"] in Scope.names


def test_epss_score():
def test_epss_score(cve_data):
assert 0 <= float(cve_data["epss"]) <= 1.0


def test_vendor_reference():
def test_vendor_reference(cve_data):
assert len(cve_data["vendor_reference"]) >= 0
3 changes: 3 additions & 0 deletions backend/env.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ IS_DEV=True
# This is the URL that the application can be reached at
FULLY_QUALIFIED_DOMAIN_NAME=http://localhost:8080

# The log level of the backend application
LOG_LEVEL=INFO

##############################################################################################################
# Setup of API keys and secrets
# Except for the NVD_API_KEY the variables can be filled with random keys!
Expand Down
6 changes: 4 additions & 2 deletions backend/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[pytest]
addopts = --nomigrations --reuse-db
DJANGO_SETTINGS_MODULE = securecheckplus.settings
addopts = --nomigrations --reuse-db -m "not nvd_integration"
DJANGO_SETTINGS_MODULE = securecheckplus.settings
markers =
nvd_integration: tests that call the real NVD API (requires internet + API key)
15 changes: 15 additions & 0 deletions backend/securecheckplus/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,18 @@ def format(self, record):
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 # 30 days

# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_IMG_SRC = (
"'self'",
"data:",
"*.interssl.com",
"www.wkoecg.at",
"*.geotrust.com",
"*.paypal.com",
"*.amazonaws.com",
"*.google-analytics.com",
"*.cloudflare.com",
"api.dicebear.com",
)
3 changes: 3 additions & 0 deletions backend/webserver/views/project_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ def post(self, request, project_id):
else:
raise InvalidValueError(project_id)

if len(request.data.get("projectName", "")) > 50:
raise InvalidValueError("Project name exceeds the maximum length of 50 characters")

return Response(f"Creation of {project_id} successful!")

except AlreadyExists as ae:
Expand Down
55 changes: 47 additions & 8 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@dicebear/bottts-neutral": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.4.4",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const boxProps = (headerComponent: React.ReactNode) => {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
justifyContent: "start",
backgroundColor: mainTheme.palette.primary.main,
borderRadius: "0.5rem",
paddingTop: headerComponent === undefined ? "2rem" : 0,
Expand Down
Loading
Loading