diff --git a/backend/.coveragerc b/backend/.coveragerc index 25dd7aa..3e273b9 100644 --- a/backend/.coveragerc +++ b/backend/.coveragerc @@ -1,8 +1,8 @@ [run] source = . -omit = frontend/*,*test*,*apps.py,*manage.py,*__init__.py,*migrations*,*asgi*,*wsgi*,*admin.py,*urls.py +omit = frontend/*,*test*,*migrations*,*asgi*,*wsgi*,*admin.py [report] exclude_lines = pragma: no cover -omit = frontend/*,*test*,*apps.py,*manage.py,*__init__.py,*migrations*,*asgi*,*wsgi*,*admin.py,*urls.py +omit = frontend/*,*test*,*migrations*,*asgi*,*wsgi*,*admin.py diff --git a/backend/analyzer/test/test_custom_parser_classes.py b/backend/analyzer/test/test_custom_parser_classes.py index c6ed206..fe077b5 100644 --- a/backend/analyzer/test/test_custom_parser_classes.py +++ b/backend/analyzer/test/test_custom_parser_classes.py @@ -2,7 +2,7 @@ import pytest -from analyzer.services.custom_parser_classes import XMLParser, PlainTextParser +from analyzer.services.custom_parser_classes import XMLParser, PlainTextParser, CustomTextParser @pytest.mark.django_db() @@ -20,3 +20,17 @@ def test_plain_text_correct_parse(self, setup): xml_parser = PlainTextParser() assert self.body == xml_parser.parse(stream=self.stream) + def test_custom_text_parser(self, setup): + custom_parser = CustomTextParser() + assert self.body == custom_parser.parse(stream=self.stream) + + def test_custom_text_parser_with_encoding(self, setup): + custom_parser = CustomTextParser() + parser_context = {"encoding": "utf-8"} + assert self.body == custom_parser.parse(stream=self.stream, parser_context=parser_context) + + def test_custom_text_parser_parse_error(self, setup): + custom_parser = CustomTextParser() + invalid_stream = io.BytesIO(b"\x80\x81\x82") + with pytest.raises(ParseError): + custom_parser.parse(stream=invalid_stream) diff --git a/backend/analyzer/test/test_cve_fetcher.py b/backend/analyzer/test/test_cve_fetcher.py index b1df86b..0f8dbcb 100644 --- a/backend/analyzer/test/test_cve_fetcher.py +++ b/backend/analyzer/test/test_cve_fetcher.py @@ -42,3 +42,21 @@ def test_epss_score(): def test_vendor_reference(): assert len(cve_data["vendor_reference"]) >= 0 + + +def test_find_vendor_reference(): + references = [ + {"url": "http://example.com", "tags": ["Vendor Advisory"]}, + {"url": "http://example2.com", "tags": []} + ] + vendor_reference = cve_fetcher._find_vendor_reference(references) + assert vendor_reference == "http://example.com" + + +def test_find_cwes(): + weaknesses = [ + {"description": [{"value": "CWE-79"}]}, + {"description": [{"value": "CWE-89"}]} + ] + cwes = cve_fetcher._find_cwes(weaknesses) + assert cwes == ["CWE-79", "CWE-89"] diff --git a/backend/analyzer/test/test_cve_manager.py b/backend/analyzer/test/test_cve_manager.py index d8e5382..c5a3d4e 100644 --- a/backend/analyzer/test/test_cve_manager.py +++ b/backend/analyzer/test/test_cve_manager.py @@ -63,3 +63,43 @@ def test_update_multiple_objects(real_cve_object, dummy_cve_object): assert 0 <= cve_objects[0].cvss <= 10 assert 0 <= cve_objects[1].cvss <= 10 + + +@pytest.mark.django_db +def test_get_single_cve_object(): + object_manager = CVEObjectManager(cve_id) + cve_object = object_manager.get() + assert isinstance(cve_object, CVEObject) + assert cve_object.cve_id == cve_id + + +@pytest.mark.django_db +def test_get_multiple_cve_objects(): + object_manager = CVEObjectManager([cve_id, cve_id2]) + cve_objects = object_manager.get() + assert isinstance(cve_objects, list) + assert len(cve_objects) == 2 + assert cve_objects[0].cve_id == cve_id + assert cve_objects[1].cve_id == cve_id2 + + +@pytest.mark.django_db +def test_update_cve_object_attributes(real_cve_object): + object_manager = CVEObjectManager(real_cve_object) + object_manager.update_cve() + cve_object = object_manager.get() + assert cve_object.cvss is not None + assert cve_object.epss is not None + assert cve_object.base_severity is not None + assert cve_object.published is not None + assert cve_object.updated is not None + assert cve_object.description is not None + assert cve_object.attack_vector is not None + assert cve_object.attack_complexity is not None + assert cve_object.privileges_required is not None + assert cve_object.user_interaction is not None + assert cve_object.confidentiality_impact is not None + assert cve_object.integrity_impact is not None + assert cve_object.availability_impact is not None + assert cve_object.scope is not None + assert cve_object.recommended_url is not None diff --git a/backend/analyzer/test/test_model.py b/backend/analyzer/test/test_model.py index ca87fb0..fc56840 100644 --- a/backend/analyzer/test/test_model.py +++ b/backend/analyzer/test/test_model.py @@ -163,3 +163,42 @@ def test_cascade_deletion(self): def test_delete_report(self): Report.objects.get(dependency=self.dependency, cve_object=self.cve).delete() self.assertEqual(0, len(Report.objects.all())) + + +class ProjectModelTestCase(TestCase): + def setUp(self): + self.project = Project.objects.create(project_id="TestProject") + + def test_dependency_count(self): + Dependency.objects.create(dependency_name="test.py", project=self.project) + self.assertEqual(self.project.dependency_count, 1) + + def test_resolved_report_count(self): + dependency = Dependency.objects.create(dependency_name="test.py", project=self.project) + cve = CVEObject.objects.create(cve_id="CVE2304-2333") + Report.objects.create(dependency=dependency, cve_object=cve, status=Status.NO_THREAT.name) + self.assertEqual(self.project.resolved_report_count, 1) + + def test_solution_distribution(self): + dependency = Dependency.objects.create(dependency_name="test.py", project=self.project) + cve = CVEObject.objects.create(cve_id="CVE2304-2333") + Report.objects.create(dependency=dependency, cve_object=cve, status=Status.NO_THREAT.name, solution="Solution1") + self.assertEqual(self.project.solution_distribution, {"Solution1": 1}) + + def test_status_distribution(self): + dependency = Dependency.objects.create(dependency_name="test.py", project=self.project) + cve = CVEObject.objects.create(cve_id="CVE2304-2333") + Report.objects.create(dependency=dependency, cve_object=cve, status=Status.REVIEW.name) + self.assertEqual(self.project.status_distribution, {Status.REVIEW.name: 1}) + + def test_calculate_risk_score(self): + dependency = Dependency.objects.create(dependency_name="test.py", project=self.project) + cve = CVEObject.objects.create(cve_id="CVE2304-2333", epss=0.5) + Report.objects.create(dependency=dependency, cve_object=cve) + self.assertEqual(self.project.calculate_risk_score, 0.5) + + def test_vulnerabilities_count(self): + dependency = Dependency.objects.create(dependency_name="test.py", project=self.project) + cve = CVEObject.objects.create(cve_id="CVE2304-2333", base_severity="High") + Report.objects.create(dependency=dependency, cve_object=cve, status=Status.REVIEW.name) + self.assertEqual(self.project.vulnerabilities_count([Status.REVIEW.name]), {"High": 1}) diff --git a/backend/analyzer/test/test_notification_manager.py b/backend/analyzer/test/test_notification_manager.py index 6d3de32..f35c74d 100644 --- a/backend/analyzer/test/test_notification_manager.py +++ b/backend/analyzer/test/test_notification_manager.py @@ -30,3 +30,17 @@ def test_mail_sent(self, db): notification_manager.notify(self.project) assert len(mail.outbox) == 1 assert mail.outbox[0].recipients()[0] == self.user.username + + def test_notify_multiple_users(self, db): + self.user3 = User.objects.create(username="test3@acme.de", notification_threshold=Threshold.HIGH.name) + UserWatchProject.objects.create(project=self.project, user=self.user3) + notification_manager.notify(self.project) + assert len(mail.outbox) == 2 + assert mail.outbox[0].recipients()[0] == self.user.username + assert mail.outbox[1].recipients()[0] == self.user3.username + + def test_notify_no_users(self, db): + self.user.notification_threshold = Threshold.CRITICAL.name + self.user.save() + notification_manager.notify(self.project) + assert len(mail.outbox) == 0 diff --git a/backend/analyzer/test/test_parser/test_owasp.py b/backend/analyzer/test/test_parser/test_owasp.py index 54939c2..5ff8207 100644 --- a/backend/analyzer/test/test_parser/test_owasp.py +++ b/backend/analyzer/test/test_parser/test_owasp.py @@ -8,3 +8,13 @@ class JSONTest(TestCase): def test_valid_json(self): json_string = get_owasp_json() self.assertEqual(TRUE_DATA, owasp_parser.parse_json(json_string)) + + def test_invalid_json(self): + invalid_json_string = '{"invalid": "json"}' + with self.assertRaises(KeyError): + owasp_parser.parse_json(invalid_json_string) + + def test_empty_json(self): + empty_json_string = '{}' + with self.assertRaises(KeyError): + owasp_parser.parse_json(empty_json_string) diff --git a/backend/analyzer/test/test_parser_manager.py b/backend/analyzer/test/test_parser_manager.py index ac0b6e1..eb703b9 100644 --- a/backend/analyzer/test/test_parser_manager.py +++ b/backend/analyzer/test/test_parser_manager.py @@ -23,3 +23,8 @@ def test_invalid_types(): def test_invalid_data_calls_message(): with pytest.raises(ParseError): ParserManager(tool_name="owasp", file_type="json").parse("") + + +def test_parse_empty_body(): + with pytest.raises(ParseError): + ParserManager(tool_name="owasp", file_type="json").parse("") diff --git a/backend/analyzer/test/test_project.py b/backend/analyzer/test/test_project.py index 7bbc244..a9c7cb3 100644 --- a/backend/analyzer/test/test_project.py +++ b/backend/analyzer/test/test_project.py @@ -35,3 +35,35 @@ def test_count_vulnerabilities(self, db): BaseSeverity.NA.name: 1 } assert true_data == counted + + def test_dependency_count(self, db): + assert self.project.dependency_count == 2 + + def test_resolved_report_count(self, db): + assert self.project.resolved_report_count == 0 + + def test_solution_distribution(self, db): + solution_distribution = self.project.solution_distribution + assert solution_distribution == { + "NO_SOLUTION_NEEDED": 0, + "SOLUTION_AVAILABLE": 0, + "SOLUTION_IMPLEMENTED": 0, + "SOLUTION_NOT_AVAILABLE": 0, + "SOLUTION_NOT_NEEDED": 0, + "SOLUTION_PARTIALLY_IMPLEMENTED": 0, + "SOLUTION_UNKNOWN": 0, + } + + def test_status_distribution(self, db): + status_distribution = self.project.status_distribution + assert status_distribution == { + "REVIEW": 5, + "NO_THREAT": 0, + "THREAT_FIXED": 0, + "THREAT_NOT_FIXED": 0, + "THREAT_UNKNOWN": 0, + } + + def test_calculate_risk_score(self, db): + risk_score = self.project.calculate_risk_score + assert risk_score == 1 - (1 - 0) * (1 - 0) * (1 - 0) * (1 - 0) * (1 - 0) diff --git a/backend/analyzer/test/test_project_manager.py b/backend/analyzer/test/test_project_manager.py index 0a8e2da..8bd1df0 100644 --- a/backend/analyzer/test/test_project_manager.py +++ b/backend/analyzer/test/test_project_manager.py @@ -77,3 +77,49 @@ def test_update_project(dummy_project): "CVE-2019-8331"} assert (set(dependency_reports2)) == {"CVE-2015-9251", "CVE-2019-11358", "CVE-2020-11022", "CVE-2020-11023"} + + +@pytest.mark.django_db +def test_generate_key(dummy_project): + project_manager = ProjectManager(dummy_project) + key = project_manager.generate_key() + assert len(key) == 43 + assert project_manager.verify_key(key) + + +@pytest.mark.django_db +def test_verify_key(dummy_project): + project_manager = ProjectManager(dummy_project) + key = project_manager.generate_key() + assert project_manager.verify_key(key) + assert not project_manager.verify_key("invalid_key") + + +@pytest.mark.django_db +def test_update_project_with_no_changes(dummy_project): + data = ParserManager(tool_name="owasp", file_type="json").parse(get_owasp_json()) + project_manager = ProjectManager(dummy_project) + project_manager.update_project(data) + assert not project_manager.has_changed(data) + + +@pytest.mark.django_db +def test_update_project_with_new_data(dummy_project): + data = ParserManager(tool_name="owasp", file_type="json").parse(get_owasp_json()) + project_manager = ProjectManager(dummy_project) + project_manager.update_project(data) + new_data = { + "new_dependency:1.0.0": { + "dependency_name": "new_dependency", + "version": "1.0.0", + "path": "path/to/new_dependency", + "license": "MIT", + "vulnerabilities": ["CVE-2021-12345"], + "package_manager": "npm" + } + } + assert project_manager.has_changed(new_data) + project_manager.update_project(new_data) + assert not project_manager.has_changed(new_data) + dependency_names = project_manager.get().dependency_set.values_list("dependency_name", flat=True) + assert "new_dependency" in dependency_names diff --git a/backend/analyzer/test/test_views.py b/backend/analyzer/test/test_views.py index 072aeec..dde09f9 100644 --- a/backend/analyzer/test/test_views.py +++ b/backend/analyzer/test/test_views.py @@ -91,3 +91,23 @@ def test_threshold_exception(self, setup): response = self.view(request) assert response.status_code == 406 + + def test_health_endpoint(self, setup): + request = self.client.get("/analyzer/health") + response = self.view(request) + assert response.status_code == 200 + assert response.content == b"I'm fine!" + + def test_analyze_report_with_no_dependencies(self, setup): + empty_report = "{}" + request = self.client.post(self.correct_url, data=empty_report, content_type=self.content_type, + HTTP_API_KEY=self.key) + response = self.view(request) + assert response.status_code == 400 + assert b"No dependencies found" in response.content + + def test_analyze_report_with_internal_server_error(self, setup): + request = self.client.post(self.correct_url, data=self.report, content_type=self.content_type, + HTTP_API_KEY=self.key) + with pytest.raises(Exception): + self.view(request) diff --git a/backend/webserver/test/test_authentication_manager.py b/backend/webserver/test/test_authentication_manager.py index b5cad24..c4b1f8a 100644 --- a/backend/webserver/test/test_authentication_manager.py +++ b/backend/webserver/test/test_authentication_manager.py @@ -40,3 +40,25 @@ def test_login(self): auth.logout(request) assert isinstance(auth.get_user(request), AnonymousUser) + + @patch("securecheckplus.settings.LDAP_HOST", None) + def test_update_group(self): + user = User.objects.create(username="TestUser") + group = Group.objects.create(name="basic") + user.groups.add(group) + update_group(user, [LDAP_BASE_GROUP_DN]) + assert user.groups.filter(name="basic").exists() + + @patch("securecheckplus.settings.LDAP_HOST", None) + def test_authenticate_with_ldap(self): + with patch("webserver.manager.ldap_adapter.LdapAdapter.authenticate_user", return_value=[LDAP_BASE_GROUP_DN]): + user = auth.authenticate(username="ldap_user", password="ldap_password") + assert user is not None + assert user.username == "ldap_user" + assert user.groups.filter(name="basic").exists() + + @patch("securecheckplus.settings.LDAP_HOST", None) + def test_authenticate_with_invalid_ldap(self): + with patch("webserver.manager.ldap_adapter.LdapAdapter.authenticate_user", return_value=None): + user = auth.authenticate(username="invalid_ldap_user", password="invalid_ldap_password") + assert user is None diff --git a/backend/webserver/test/test_authorization_manager.py b/backend/webserver/test/test_authorization_manager.py index 8eec816..cc7b9b7 100644 --- a/backend/webserver/test/test_authorization_manager.py +++ b/backend/webserver/test/test_authorization_manager.py @@ -1,11 +1,10 @@ - import pytest from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -from webserver.manager.authorization_manager import permission_required +from webserver.manager.authorization_manager import permission_required, IsPut, IsPost, IsGet from webserver.models import User @@ -49,3 +48,21 @@ def test_have_access_after_group_change(self, db): self.user.groups.add(self.admin_group) self.user.save() assert permission_manager.has_permission(permission_manager, request, view=APIView.as_view()) + + def test_is_put_permission(self, db): + permission_manager = IsPut() + request = APIRequestFactory(enforce_csrf_checks=True).put("/", data="", content_type=self.request_content_type) + request.user = self.user + assert permission_manager.has_permission(request=request, view=APIView.as_view()) + + def test_is_post_permission(self, db): + permission_manager = IsPost() + request = APIRequestFactory(enforce_csrf_checks=True).post("/", data="", content_type=self.request_content_type) + request.user = self.user + assert permission_manager.has_permission(request=request, view=APIView.as_view()) + + def test_is_get_permission(self, db): + permission_manager = IsGet() + request = APIRequestFactory(enforce_csrf_checks=True).get("/", content_type=self.request_content_type) + request.user = self.user + assert permission_manager.has_permission(request=request, view=APIView.as_view()) diff --git a/backend/webserver/test/test_model.py b/backend/webserver/test/test_model.py index a20ddcb..66d5763 100644 --- a/backend/webserver/test/test_model.py +++ b/backend/webserver/test/test_model.py @@ -6,6 +6,81 @@ from webserver.models import User, UserWatchProject +class UserWatchProjectTestCase(TestCase): + def setUp(self): + self.project_id = "SECURECHECKPLUS" + self.project_name = "SecureCheckPlus" + Project.objects.create(project_id=self.project_id, project_name=self.project_name) + user = User.objects.create(username="User1@acme.de") + user2 = User.objects.create(username="User2@acme.de") + project = Project.objects.get(project_id=self.project_id) + UserWatchProject.objects.create(user=user, project=project) + UserWatchProject.objects.create(user=user2, project=project) + + # Test if saved - Project can have multiple users + def test_project_have_multiple_user(self): + self.assertEqual(2, len(UserWatchProject.objects.all())) + + # Test if associated Entities are properly deleted + def test_delete_cascade(self): + Project.objects.get(project_id=self.project_id).delete() + self.assertEqual(0, len(UserWatchProject.objects.all())) + + +class UserTestCase(TestCase): + def setUp(self): + self.email = "user@acme.de" + user = User.objects.create(username=self.email, notification_threshold=Threshold.CRITICAL.name) + project = Project.objects.create(project_id="Test1") + project2 = Project.objects.create(project_id="Test2") + UserWatchProject.objects.create(user=user, project=project) + UserWatchProject.objects.create(user=user, project=project2) + + # Test if Entity is properly saved + def test_save_user(self): + user = User.objects.get(username=self.email) + self.assertEqual(user.username, self.email) + + # Test if many-to-many relationship is properly saved + def test_user_have_multiple_object(self): + self.assertEqual(2, len(UserWatchProject.objects.all())) + + # Test if unique constraint will trigger + def test_unique_email(self): + try: + User.objects.create(username=self.email) + self.assertTrue(False) + except IntegrityError: + self.assertTrue(True) + + # Test if Entity is deleted properly + def test_delete_user(self): + User.objects.get(username=self.email).delete() + self.assertEqual(0, len(User.objects.filter(username=self.email))) + + # Test if associated Entities will delete properly + def test_delete_cascade(self): + User.objects.get(username=self.email).delete() + self.assertEqual(0, len(UserWatchProject.objects.all())) + + def test_favorites(self): + user = User.objects.get(username=self.email) + self.assertEqual(len(user.favorite_projects()), 0) + + user_watch_project = UserWatchProject.objects.get(user=user, project__project_id="Test1") + user_watch_project.favorite = True + user_watch_project.save() + self.assertEqual(len(user.favorite_projects()), 1) + + def test_clean_up_and_recent_projects(self): + user = User.objects.get(username=self.email) + user._clean_up(5) + self.assertEqual(len(user.recent_projects()), 2) + + user._clean_up(1) + self.assertEqual(len(user.recent_projects()), 1) + + class UserWatchProjectTestCase(TestCase): def setUp(self): self.project_id = "SECURECHECKPLUS"