From 6bf07fa9b30d77462e9f7d030a941235c50e608a Mon Sep 17 00:00:00 2001 From: Divya Prakash Date: Sun, 2 Aug 2026 16:03:25 +0530 Subject: [PATCH] Added EU CRA compliance framework --- ak_vendor/compliances/eucra.py | 114 ++++++++++++++++++++ ak_vendor/locale/ja/LC_MESSAGES/django.po | 87 ++++++++++++++-- ak_vendor/report/__init__.py | 2 + ak_vendor/report/report.py | 21 ++++ ak_vendor/report_sample1.json | 30 ++++++ ak_vendor/templates/report_template.html | 20 +++- tests/test_eucra.py | 121 ++++++++++++++++++++++ 7 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 ak_vendor/compliances/eucra.py create mode 100644 tests/test_eucra.py diff --git a/ak_vendor/compliances/eucra.py b/ak_vendor/compliances/eucra.py new file mode 100644 index 0000000..7967916 --- /dev/null +++ b/ak_vendor/compliances/eucra.py @@ -0,0 +1,114 @@ +from gettext import gettext as _ + +# Reference: https://eur-lex.europa.eu/eli/reg/2024/2847/oj + +data = [ + { + "id": "cra_i_2_a", + "code": "Annex I, Part I, point (2)(a)", + "title": _( + "be made available on the market without known exploitable vulnerabilities;" + ), + }, + { + "id": "cra_i_2_b", + "code": "Annex I, Part I, point (2)(b)", + "title": _( + "be made available on the market with a secure by default configuration, " + "unless otherwise agreed between manufacturer and business user in relation " + "to a tailor-made product with digital elements, including the possibility " + "to reset the product to its original state;" + ), + }, + { + "id": "cra_i_2_d", + "code": "Annex I, Part I, point (2)(d)", + "title": _( + "ensure protection from unauthorised access by appropriate control " + "mechanisms, including but not limited to authentication, identity or " + "access management systems, and report on possible unauthorised access;" + ), + }, + { + "id": "cra_i_2_e", + "code": "Annex I, Part I, point (2)(e)", + "title": _( + "protect the confidentiality of stored, transmitted or otherwise processed " + "data, personal or other, such as by encrypting relevant data at rest or in " + "transit by state of the art mechanisms, and by using other technical means;" + ), + }, + { + "id": "cra_i_2_f", + "code": "Annex I, Part I, point (2)(f)", + "title": _( + "protect the integrity of stored, transmitted or otherwise processed data, " + "personal or other, commands, programs and configuration against any " + "manipulation or modification not authorised by the user, and report on " + "corruptions;" + ), + }, + { + "id": "cra_i_2_g", + "code": "Annex I, Part I, point (2)(g)", + "title": _( + "process only data, personal or other, that are adequate, relevant and " + "limited to what is necessary in relation to the intended purpose of the " + "product with digital elements (data minimisation);" + ), + }, + { + "id": "cra_i_2_h", + "code": "Annex I, Part I, point (2)(h)", + "title": _( + "protect the availability of essential and basic functions, also after an " + "incident, including through resilience and mitigation measures against " + "denial-of-service attacks;" + ), + }, + { + "id": "cra_i_2_j", + "code": "Annex I, Part I, point (2)(j)", + "title": _( + "be designed, developed and produced to limit attack surfaces, including " + "external interfaces;" + ), + }, + { + "id": "cra_i_2_k", + "code": "Annex I, Part I, point (2)(k)", + "title": _( + "be designed, developed and produced to reduce the impact of an incident " + "using appropriate exploitation mitigation mechanisms and techniques;" + ), + }, + { + "id": "cra_ii_2", + "code": "Annex I, Part II, point (2)", + "title": _( + "in relation to the risks posed to products with digital elements, address " + "and remediate vulnerabilities without delay, including by providing " + "security updates; where technically feasible, new security updates shall " + "be provided separately from functionality updates;" + ), + }, +] + + +class EUCRA: + """Represents a single EU CRA (Regulation (EU) 2024/2847) annex entry.""" + + def __init__(self, id, code, title): + self.pk = id + self.id = id + self.code = code + self.title = title + + def __str__(self): + return f"{self.id} - {self.code} - {self.title}" + + def __repr__(self): + return "" % self.__str__() + + +EUCRA_DATA = {d["id"]: EUCRA(**d) for d in data} diff --git a/ak_vendor/locale/ja/LC_MESSAGES/django.po b/ak_vendor/locale/ja/LC_MESSAGES/django.po index e91311a..cbc1cfe 100644 --- a/ak_vendor/locale/ja/LC_MESSAGES/django.po +++ b/ak_vendor/locale/ja/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Appknox\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-07-21 16:07+0100\n" +"POT-Creation-Date: 2026-08-02 16:01+0530\n" "Language: ja\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -2014,6 +2014,77 @@ msgid "" "feasible, and penetration testing." msgstr "" +#: ak_vendor/compliances/eucra.py:10 +#, fuzzy +#| msgid "Using Components with Known Vulnerabilities" +msgid "" +"be made available on the market without known exploitable vulnerabilities;" +msgstr "既知の脆弱性を持つコンポーネントの使用" + +#: ak_vendor/compliances/eucra.py:17 +msgid "" +"be made available on the market with a secure by default configuration, " +"unless otherwise agreed between manufacturer and business user in relation " +"to a tailor-made product with digital elements, including the possibility to " +"reset the product to its original state;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:27 +msgid "" +"ensure protection from unauthorised access by appropriate control " +"mechanisms, including but not limited to authentication, identity or access " +"management systems, and report on possible unauthorised access;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:36 +msgid "" +"protect the confidentiality of stored, transmitted or otherwise processed " +"data, personal or other, such as by encrypting relevant data at rest or in " +"transit by state of the art mechanisms, and by using other technical means;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:45 +msgid "" +"protect the integrity of stored, transmitted or otherwise processed data, " +"personal or other, commands, programs and configuration against any " +"manipulation or modification not authorised by the user, and report on " +"corruptions;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:55 +msgid "" +"process only data, personal or other, that are adequate, relevant and " +"limited to what is necessary in relation to the intended purpose of the " +"product with digital elements (data minimisation);" +msgstr "" + +#: ak_vendor/compliances/eucra.py:64 +msgid "" +"protect the availability of essential and basic functions, also after an " +"incident, including through resilience and mitigation measures against " +"denial-of-service attacks;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:73 +msgid "" +"be designed, developed and produced to limit attack surfaces, including " +"external interfaces;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:81 +msgid "" +"be designed, developed and produced to reduce the impact of an incident " +"using appropriate exploitation mitigation mechanisms and techniques;" +msgstr "" + +#: ak_vendor/compliances/eucra.py:89 +msgid "" +"in relation to the risks posed to products with digital elements, address " +"and remediate vulnerabilities without delay, including by providing security " +"updates; where technically feasible, new security updates shall be provided " +"separately from functionality updates;" +msgstr "" + #: ak_vendor/compliances/gdpr.py:8 msgid "Data protection by design and by default" msgstr "" @@ -5090,19 +5161,19 @@ msgid "" "security controls should periodically be measured and evaluated." msgstr "" -#: ak_vendor/report/report.py:1083 +#: ak_vendor/report/report.py:1104 msgid "Static" msgstr "静的" -#: ak_vendor/report/report.py:1084 +#: ak_vendor/report/report.py:1105 msgid "Dynamic" msgstr "動的" -#: ak_vendor/report/report.py:1085 +#: ak_vendor/report/report.py:1106 msgid "API" msgstr "" -#: ak_vendor/report/report.py:1086 +#: ak_vendor/report/report.py:1107 msgid "Manual" msgstr "手動" @@ -5524,10 +5595,14 @@ msgstr "HIPAA" msgid "GDPR" msgstr "" -#: ak_vendor/templates/report_template.html:1466 +#: ak_vendor/templates/report_template.html:1690 msgid "DORA" msgstr "" +#: ak_vendor/templates/report_template.html:1708 +msgid "EU CRA" +msgstr "" + #: ak_vendor/templates/report_template.html:1484 msgid "NIST SP 800-53" msgstr "" diff --git a/ak_vendor/report/__init__.py b/ak_vendor/report/__init__.py index cfe72ac..e3f198c 100644 --- a/ak_vendor/report/__init__.py +++ b/ak_vendor/report/__init__.py @@ -2,6 +2,7 @@ ASVS, CWE, DORA, + EUCRA, GDPR, HIPAA, MASVS, @@ -53,6 +54,7 @@ HIPAA.__name__, DORA.__name__, GDPR.__name__, + EUCRA.__name__, NISTSP80053.__name__, NISTSP800171.__name__, SAMA.__name__, diff --git a/ak_vendor/report/report.py b/ak_vendor/report/report.py index 092cc6e..7fb69ce 100644 --- a/ak_vendor/report/report.py +++ b/ak_vendor/report/report.py @@ -631,6 +631,12 @@ class DORA: title = attr.ib(type=str) +@attr.s +class EUCRA: + code = attr.ib(type=str) + title = attr.ib(type=str) + + @attr.s class NISTSP80053: code = attr.ib(type=str) @@ -664,6 +670,7 @@ class Regulatory: hipaa = attr.ib(factory=list, type=List[dict]) gdpr = attr.ib(factory=list, type=List[dict]) dora = attr.ib(factory=list, type=List[dict]) + eucra = attr.ib(factory=list, type=List[dict]) nistsp80053 = attr.ib(factory=list, type=List[dict]) nistsp800171 = attr.ib(factory=list, type=List[dict]) sama = attr.ib(factory=list, type=List[dict]) @@ -689,6 +696,7 @@ def from_json(cls, data): hipaa=[HIPAA.from_json(hipaa) for hipaa in data.get("hipaa", [])], gdpr=[GDPR(**gdpr) for gdpr in data.get("gdpr", [])], dora=[DORA(**dora) for dora in data.get("dora", [])], + eucra=[EUCRA(**eucra) for eucra in data.get("eucra", [])], nistsp80053=[ NISTSP80053(**nistsp80053) for nistsp80053 in data.get("nistsp80053", []) @@ -763,6 +771,10 @@ def create_gdpr(cls, code: str, title: str) -> GDPR: def create_dora(cls, code: str, title: str) -> DORA: return DORA(code=code, title=title) + @classmethod + def create_eucra(cls, code: str, title: str) -> EUCRA: + return EUCRA(code=code, title=title) + @classmethod def create_nistsp80053(cls, code: str, title: str) -> NISTSP80053: return NISTSP80053(code=code, title=title) @@ -825,6 +837,10 @@ def add_dora(self, dora: DORA) -> List[DORA]: self.dora.append(dora) return self.dora + def add_eucra(self, eucra: EUCRA) -> List[EUCRA]: + self.eucra.append(eucra) + return self.eucra + def add_nistsp80053(self, nistsp80053: NISTSP80053) -> List[NISTSP80053]: self.nistsp80053.append(nistsp80053) return self.nistsp80053 @@ -875,6 +891,7 @@ class Analysis: cwe=[], gdpr=[], dora=[], + eucra=[], mstg=[], masvs=[], owaspapi2023=[], @@ -940,6 +957,7 @@ def create_regulatory( hipaa: List[dict] = None, gdpr: List[dict] = None, dora: List[dict] = None, + eucra: List[dict] = None, owaspapi2023: List[dict] = None, owaspmobile2024: List[dict] = None, nistsp80053: List[dict] = None, @@ -960,6 +978,8 @@ def create_regulatory( gdpr = [] if dora is None: dora = [] + if eucra is None: + eucra = [] if hipaa is None: hipaa = [] if pcidss is None: @@ -989,6 +1009,7 @@ def create_regulatory( hipaa=hipaa, gdpr=gdpr, dora=dora, + eucra=eucra, nistsp80053=nistsp80053, nistsp800171=nistsp800171, sama=sama, diff --git a/ak_vendor/report_sample1.json b/ak_vendor/report_sample1.json index 6d927d9..437bf19 100644 --- a/ak_vendor/report_sample1.json +++ b/ak_vendor/report_sample1.json @@ -159,6 +159,16 @@ "code": "Art. 25(1)", "title": "Evidenced via vulnerability assessments/scans and penetration testing." } + ], + "eucra": [ + { + "code": "Annex I, Part I, point (2)(e)", + "title": "protect the confidentiality of stored, transmitted or otherwise processed data, personal or other, such as by encrypting relevant data at rest or in transit by state of the art mechanisms, and by using other technical means;" + }, + { + "code": "Annex I, Part I, point (2)(f)", + "title": "protect the integrity of stored, transmitted or otherwise processed data, personal or other, commands, programs and configuration against any manipulation or modification not authorised by the user, and report on corruptions;" + } ] }, "risk": { @@ -379,6 +389,16 @@ "code": "Art. 25(1)", "title": "Evidenced via vulnerability assessments/scans, source-code reviews where feasible, and penetration testing." } + ], + "eucra": [ + { + "code": "Annex I, Part I, point (2)(d)", + "title": "ensure protection from unauthorised access by appropriate control mechanisms, including but not limited to authentication, identity or access management systems, and report on possible unauthorised access;" + }, + { + "code": "Annex I, Part I, point (2)(j)", + "title": "be designed, developed and produced to limit attack surfaces, including external interfaces;" + } ] }, "risk": { @@ -827,6 +847,16 @@ "code": "Art. 25(1)", "title": "Evidenced via vulnerability assessments/security testing." } + ], + "eucra": [ + { + "code": "Annex I, Part I, point (2)(e)", + "title": "protect the confidentiality of stored, transmitted or otherwise processed data, personal or other, such as by encrypting relevant data at rest or in transit by state of the art mechanisms, and by using other technical means;" + }, + { + "code": "Annex I, Part II, point (2)", + "title": "in relation to the risks posed to products with digital elements, address and remediate vulnerabilities without delay, including by providing security updates; where technically feasible, new security updates shall be provided separately from functionality updates;" + } ] }, "risk": { diff --git a/ak_vendor/templates/report_template.html b/ak_vendor/templates/report_template.html index f22211d..0ac38df 100644 --- a/ak_vendor/templates/report_template.html +++ b/ak_vendor/templates/report_template.html @@ -1474,7 +1474,7 @@

{% trans "Regulatory" %}

@@ -1708,6 +1708,24 @@

{% trans "Regulatory" %}

{% endif %}
+
+ {% if analysis.regulatory.eucra %} +
+ {% trans "EU CRA" %} +
+ + + {% for eucra in analysis.regulatory.eucra %} + + + + + {% endfor %} + +
{{ eucra.code }}{{ eucra.title }}
+ {% endif %} +
+
{% if analysis.regulatory.nistsp80053 %}
diff --git a/tests/test_eucra.py b/tests/test_eucra.py new file mode 100644 index 0000000..4541c62 --- /dev/null +++ b/tests/test_eucra.py @@ -0,0 +1,121 @@ +"""Tests specific to the EU CRA compliance module.""" + +from collections import Counter + + +class TestEUCRA: + """Tests for EU CRA catalogue structure and helpers.""" + + def setup_method(self): + """Load the EUCRA module once per test.""" + import ak_vendor.compliances.eucra as eucra_module + + self.data = eucra_module.data + self.EUCRA = eucra_module.EUCRA + self.EUCRA_DATA = eucra_module.EUCRA_DATA + + # -- data list ----------------------------------------------------------- + + def test_entry_count(self): + """Catalog must have exactly 10 entries.""" + assert len(self.data) == 10 + + def test_all_entries_have_non_empty_title(self): + """Every entry must carry a non-empty title string.""" + for item in self.data: + assert "title" in item, f"Entry {item['id']} missing 'title'" + assert item["title"], f"Entry {item['id']} has empty 'title'" + + def test_id_format(self): + """All ids must follow the 'cra_' prefix naming convention.""" + for item in self.data: + assert item["id"].startswith("cra_"), ( + f"Entry id '{item['id']}' does not follow the 'cra_' prefix convention" + ) + + def test_codes_are_unique(self): + """Each annex point maps to exactly one catalogue entry.""" + codes = [item["code"] for item in self.data] + duplicated = {c for c in codes if codes.count(c) > 1} + assert not duplicated, ( + f"Expected unique EU CRA codes, found duplicates: {duplicated}" + ) + + def test_known_clause_ids(self): + """Pin the expected annex clause ids from the mapping document.""" + expected_ids = { + "cra_i_2_a", + "cra_i_2_b", + "cra_i_2_d", + "cra_i_2_e", + "cra_i_2_f", + "cra_i_2_g", + "cra_i_2_h", + "cra_i_2_j", + "cra_i_2_k", + "cra_ii_2", + } + assert {item["id"] for item in self.data} == expected_ids + + def test_code_counts(self): + """Every code appears exactly once.""" + counts = Counter(item["code"] for item in self.data) + assert all(count == 1 for count in counts.values()) + + # -- EUCRA class --------------------------------------------------------- + + def test_class_attributes(self): + """EUCRA instance exposes pk, id, code, and title with correct values.""" + entry = self.EUCRA( + id="cra_i_2_d", + code="Annex I, Part I, point (2)(d)", + title="Test title", + ) + assert entry.pk == "cra_i_2_d" + assert entry.id == "cra_i_2_d" + assert entry.code == "Annex I, Part I, point (2)(d)" + assert entry.title == "Test title" + + def test_str(self): + """__str__ returns 'id - code - title'.""" + entry = self.EUCRA( + id="cra_i_2_d", + code="Annex I, Part I, point (2)(d)", + title="Test title", + ) + assert str(entry) == "cra_i_2_d - Annex I, Part I, point (2)(d) - Test title" + + def test_repr(self): + """__repr__ wraps __str__ in angle brackets.""" + entry = self.EUCRA( + id="cra_i_2_d", + code="Annex I, Part I, point (2)(d)", + title="Test title", + ) + assert ( + repr(entry) + == "" + ) + + # -- EUCRA_DATA dict ----------------------------------------------------- + + def test_eucra_data_has_all_entries(self): + """EUCRA_DATA must contain one key per data entry.""" + assert len(self.EUCRA_DATA) == len(self.data) + + def test_eucra_data_keyed_by_id(self): + """Every data id must be a key in EUCRA_DATA.""" + for item in self.data: + assert item["id"] in self.EUCRA_DATA, ( + f"'{item['id']}' missing from EUCRA_DATA" + ) + + def test_eucra_data_values_are_eucra_instances(self): + """All EUCRA_DATA values must be EUCRA instances with correct attributes.""" + for item in self.data: + obj = self.EUCRA_DATA[item["id"]] + assert isinstance(obj, self.EUCRA) + assert obj.id == item["id"] + assert obj.pk == item["id"] + assert obj.code == item["code"] + assert obj.title == item["title"]