diff --git a/Makefile b/Makefile index af885c5c..45f8bb72 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ MKDOCS_PORT=8765 DOCKER_DIR=docker # Targets -.PHONY: all test docs docker_test clean help +.PHONY: all test docs docker_test clean help mdlint_fix up down regenerate_json all: help @@ -31,6 +31,11 @@ down: @echo "Stopping Docker services..." pushd $(DOCKER_DIR) && docker-compose down +regenerate_json: + @echo "Regenerating JSON files..." + rm -rf data/json/decision_points + export PYTHONPATH=$(PWD)/src && ./src/ssvc/doctools.py --jsondir=./data/json/decision_points --overwrite + clean: @echo "Cleaning up Docker resources..." pushd $(DOCKER_DIR) && docker-compose down --rmi local || true @@ -40,9 +45,14 @@ help: @echo "" @echo "Targets:" @echo " all - Display this help message" - @echo " mdlint_fix - Run markdownlint with --fix" - @echo " test - Run the tests in a local shell" - @echo " docs - Build and run the docs Docker service" - @echo " docker_test - Run the tests in a Docker container" - @echo " clean - Remove Docker containers and images" - @echo " help - Display this help message" + @echo " mdlint_fix - Run markdownlint with fix" + @echo " test - Run tests locally" + @echo " docker_test - Run tests in Docker" + @echo " docs - Build and run documentation in Docker" + @echo " up - Start Docker services" + @echo " down - Stop Docker services" + @echo " regenerate_json - Regenerate JSON files from python modules" + @echo " clean - Clean up Docker resources" + @echo " help - Display this help message" + + diff --git a/README.md b/README.md index 81b2ef41..770ee0fe 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,6 @@ Options for running the test suite are provided below. | Make, ~~Docker~~ | `make test` | runs in host OS | | ~~Make~~, ~~Docker~~ | `pytest src/test` | runs in host OS | - ## Environment Variables If you encounter a problem with the `ssvc` module not being found, you may need to set the `PYTHONPATH` environment variable. diff --git a/data/json/outcomes/CISA.json b/data/json/decision_points/cisa/cisa_levels_1_0_0.json similarity index 97% rename from data/json/outcomes/CISA.json rename to data/json/decision_points/cisa/cisa_levels_1_0_0.json index c4ebbd2a..e836c69c 100644 --- a/data/json/outcomes/CISA.json +++ b/data/json/decision_points/cisa/cisa_levels_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "CISA Levels", "description": "The CISA outcome group. CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", - "outcomes": [ + "namespace": "cisa", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CISA", + "values": [ { "key": "T", "name": "Track", @@ -25,4 +27,4 @@ "description": "The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. CISA recommends remediating Act vulnerabilities as soon as possible." } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/cisa/mission_prevalence_1_0_0.json b/data/json/decision_points/cisa/mission_prevalence_1_0_0.json new file mode 100644 index 00000000..50a06e4b --- /dev/null +++ b/data/json/decision_points/cisa/mission_prevalence_1_0_0.json @@ -0,0 +1,25 @@ +{ + "name": "Mission Prevalence", + "description": "Prevalence of the mission essential functions", + "namespace": "cisa", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "MP", + "values": [ + { + "key": "M", + "name": "Minimal", + "description": "Neither Support nor Essential apply. The vulnerable component may be used within the entities, but it is not used as a mission-essential component, nor does it provide impactful support to mission-essential functions." + }, + { + "key": "S", + "name": "Support", + "description": "The vulnerable component only supports MEFs for two or more entities." + }, + { + "key": "E", + "name": "Essential", + "description": "The vulnerable component directly provides capabilities that constitute at least one MEF for at least one entity; component failure may (but does not necessarily) lead to overall mission failure." + } + ] +} diff --git a/data/json/decision_points/cvss/availability_impact_2_0_1.json b/data/json/decision_points/cvss/availability_impact_2_0_1.json deleted file mode 100644 index e815d46a..00000000 --- a/data/json/decision_points/cvss/availability_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "A", - "name": "Availability Impact", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "There is reduced performance or interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users. The resources in the Vulnerable System are either partially available all of the time, or fully available only some of the time, but overall there is no direct, serious consequence to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - } - ] -} diff --git a/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json b/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json deleted file mode 100644 index 4c72a5d5..00000000 --- a/data/json/decision_points/cvss/confidentiality_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "C", - "name": "Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of confidentiality within the impacted component." - }, - { - "key": "L", - "name": "Low", - "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is constrained. The information disclosure does not cause a direct, serious loss to the impacted component." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of confidentiality, resulting in all resources within the impacted component being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact. For example, an attacker steals the administrator's password, or private encryption keys of a web server." - } - ] -} diff --git a/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json b/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json new file mode 100644 index 00000000..82135a43 --- /dev/null +++ b/data/json/decision_points/cvss/cvss_qualitative_severity_rating_scale_1_0_0.json @@ -0,0 +1,35 @@ +{ + "name": "CVSS Qualitative Severity Rating Scale", + "description": "The CVSS Qualitative Severity Rating Scale group.", + "namespace": "cvss", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CVSS", + "values": [ + { + "key": "N", + "name": "None", + "description": "None (0.0)" + }, + { + "key": "L", + "name": "Low", + "description": "Low (0.1-3.9)" + }, + { + "key": "M", + "name": "Medium", + "description": "Medium (4.0-6.9)" + }, + { + "key": "H", + "name": "High", + "description": "High (7.0-8.9)" + }, + { + "key": "C", + "name": "Critical", + "description": "Critical (9.0-10.0)" + } + ] +} diff --git a/data/json/decision_points/cvss/integrity_impact_2_0_1.json b/data/json/decision_points/cvss/integrity_impact_2_0_1.json deleted file mode 100644 index 59579fbd..00000000 --- a/data/json/decision_points/cvss/integrity_impact_2_0_1.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "I", - "name": "Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of integrity within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of integrity, or a complete loss of protection." - } - ] -} diff --git a/data/json/decision_points/cvss/integrity_requirement_1_0_1.json b/data/json/decision_points/cvss/integrity_requirement_1_0_1.json deleted file mode 100644 index 4c8e1762..00000000 --- a/data/json/decision_points/cvss/integrity_requirement_1_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "1.0.1", - "schemaVersion": "1-0-1", - "key": "IR", - "name": "Integrity Requirement", - "description": "This metric enables the consumer to customize the assessment depending on the importance of the affected IT asset to the analyst’s organization, measured in terms of Confidentiality.", - "values": [ - { - "key": "L", - "name": "Low", - "description": "Loss of integrity is likely to have only a limited adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "M", - "name": "Medium", - "description": "Loss of integrity is likely to have a serious adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "H", - "name": "High", - "description": "Loss of integrity is likely to have a catastrophic adverse effect on the organization or individuals associated with the organization (e.g., employees, customers)." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json b/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json deleted file mode 100644 index 793c5579..00000000 --- a/data/json/decision_points/cvss/modified_availability_impact_2_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "MA", - "name": "Modified Availability Impact", - "description": "This metric measures the impact to the availability of the impacted system resulting from a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "There is reduced performance or interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users. The resources in the Vulnerable System are either partially available all of the time, or fully available only some of the time, but overall there is no direct, serious consequence to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is total loss of availability, resulting in the attacker being able to fully deny access to resources in the impacted component; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json index 786f0390..1bd3acd8 100644 --- a/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_0.json @@ -8,7 +8,7 @@ "values": [ { "key": "N", - "name": "Negligible", + "name": "None", "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." }, { diff --git a/data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json similarity index 81% rename from data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json rename to data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json index d8f83c65..bed5818f 100644 --- a/data/json/decision_points/cvss/modified_subsequent_availability_impact_1_0_0.json +++ b/data/json/decision_points/cvss/modified_availability_impact_to_the_subsequent_system_1_0_1.json @@ -1,15 +1,15 @@ { + "name": "Modified Availability Impact to the Subsequent System", + "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "namespace": "cvss", - "version": "1.0.0", + "version": "1.0.1", "schemaVersion": "1-0-1", "key": "MSA", - "name": "Modified Subsequent Availability Impact", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", "values": [ { "key": "N", "name": "Negligible", - "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." + "description": "There is negligible impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." }, { "key": "L", diff --git a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json similarity index 52% rename from data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json rename to data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json index 027f96a0..3e4adaa2 100644 --- a/data/json/decision_points/cvss/modified_confidentiality_impact_2_0_1.json +++ b/data/json/decision_points/cvss/modified_confidentiality_impact_to_the_subsequent_system_1_0_1.json @@ -1,25 +1,25 @@ { + "name": "Modified Confidentiality Impact to the Subsequent System", + "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. The resulting score is greatest when the loss to the system is highest.", "namespace": "cvss", - "version": "2.0.1", + "version": "1.0.1", "schemaVersion": "1-0-1", - "key": "MC", - "name": "Modified Confidentiality Impact", - "description": "This metric measures the impact to the confidentiality of the information managed by the system due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.", + "key": "MSC", "values": [ { "key": "N", - "name": "None", - "description": "There is no loss of confidentiality within the impacted component." + "name": "Negligible", + "description": "There is negligible loss of confidentiality within the Subsequent System or all confidentiality impact is constrained to the Vulnerable System." }, { "key": "L", "name": "Low", - "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is constrained. The information disclosure does not cause a direct, serious loss to the impacted component." + "description": "There is some loss of confidentiality. Access to some restricted information is obtained, but the attacker does not have control over what information is obtained, or the amount or kind of loss is limited. The information disclosure does not cause a direct, serious loss to the Subsequent System." }, { "key": "H", "name": "High", - "description": "There is total loss of confidentiality, resulting in all resources within the impacted component being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact. For example, an attacker steals the administrator's password, or private encryption keys of a web server." + "description": "There is a total loss of confidentiality, resulting in all resources within the Subsequent System being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact." }, { "key": "X", diff --git a/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json b/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json deleted file mode 100644 index a02b0fe3..00000000 --- a/data/json/decision_points/cvss/modified_integrity_impact_2_0_1.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "namespace": "cvss", - "version": "2.0.1", - "schemaVersion": "1-0-1", - "key": "MI", - "name": "Modified Integrity Impact", - "description": "This metric measures the impact to integrity of a successfully exploited vulnerability.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no loss of integrity within the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Vulnerable System." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of integrity, or a complete loss of protection." - }, - { - "key": "X", - "name": "Not Defined", - "description": "This metric value is not defined. See CVSS documentation for details." - } - ] -} diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json index 719e36b4..6af70ddc 100644 --- a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_0.json @@ -8,7 +8,7 @@ "values": [ { "key": "N", - "name": "Negligible", + "name": "None", "description": "There is no loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." }, { @@ -25,11 +25,6 @@ "key": "X", "name": "Not Defined", "description": "This metric value is not defined. See CVSS documentation for details." - }, - { - "key": "S", - "name": "Safety", - "description": "The Safety metric value measures the impact regarding the Safety of a human actor or participant that can be predictably injured as a result of the vulnerability being exploited." } ] } diff --git a/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json new file mode 100644 index 00000000..7e0c391a --- /dev/null +++ b/data/json/decision_points/cvss/modified_integrity_impact_to_the_subsequent_system_1_0_1.json @@ -0,0 +1,35 @@ +{ + "name": "Modified Integrity Impact to the Subsequent System", + "description": "This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of a system is impacted when an attacker causes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging). The resulting score is greatest when the consequence to the system is highest.", + "namespace": "cvss", + "version": "1.0.1", + "schemaVersion": "1-0-1", + "key": "MSI", + "values": [ + { + "key": "N", + "name": "Negligible", + "description": "There is negligible loss of integrity within the Subsequent System or all integrity impact is constrained to the Vulnerable System." + }, + { + "key": "L", + "name": "Low", + "description": "Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact to the Subsequent System." + }, + { + "key": "H", + "name": "High", + "description": "There is a total loss of integrity, or a complete loss of protection. For example, the attacker is able to modify any/all files protected by the Subsequent System. Alternatively, only some files can be modified, but malicious modification would present a direct, serious consequence to the Subsequent System." + }, + { + "key": "X", + "name": "Not Defined", + "description": "This metric value is not defined. See CVSS documentation for details." + }, + { + "key": "S", + "name": "Safety", + "description": "The Safety metric value measures the impact regarding the Safety of a human actor or participant that can be predictably injured as a result of the vulnerability being exploited." + } + ] +} diff --git a/data/json/decision_points/cvss/safety_1_0_0.json b/data/json/decision_points/cvss/safety_1_0_0.json index 987de4d0..b1505feb 100644 --- a/data/json/decision_points/cvss/safety_1_0_0.json +++ b/data/json/decision_points/cvss/safety_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "cvss", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "S", + "key": "SF", "values": [ { "key": "X", diff --git a/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json b/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json deleted file mode 100644 index a7ed8c04..00000000 --- a/data/json/decision_points/cvss/subsequent_availability_impact_1_0_0.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "namespace": "cvss", - "version": "1.0.0", - "schemaVersion": "1-0-1", - "key": "SA", - "name": "Subsequent Availability Impact", - "description": "This metric measures the impact on availability a successful exploit of the vulnerability will have on the Subsequent System.", - "values": [ - { - "key": "N", - "name": "None", - "description": "There is no impact to availability within the Subsequent System or all availability impact is constrained to the Vulnerable System." - }, - { - "key": "L", - "name": "Low", - "description": "Performance is reduced or there are interruptions in resource availability. Even if repeated exploitation of the vulnerability is possible, the attacker does not have the ability to completely deny service to legitimate users." - }, - { - "key": "H", - "name": "High", - "description": "There is a total loss of availability, resulting in the attacker being able to fully deny access to resources in the Subsequent System; this loss is either sustained (while the attacker continues to deliver the attack) or persistent (the condition persists even after the attack has completed)." - } - ] -} diff --git a/data/json/decision_points/human_impact_1_0_0.json b/data/json/decision_points/human_impact_1_0_0.json deleted file mode 100644 index 051c3789..00000000 --- a/data/json/decision_points/human_impact_1_0_0.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schemaVersion": "1-0-1", - "namespace": "ssvc", - "version": "1.0.0", - "key": "HI", - "name": "Human Impact", - "description": "Human Impact is a combination of Safety and Mission impacts.", - "values": [ - { - "key": "L", - "name": "Low", - "description": "Safety Impact:(None OR Minor) AND Mission Impact:(None OR Degraded OR Crippled)" - }, - { - "key": "M", - "name": "Medium", - "description": "(Safety Impact:(None OR Minor) AND Mission Impact:MEF Failure) OR (Safety Impact:Major AND Mission Impact:(None OR Degraded OR Crippled))" - }, - { - "key": "H", - "name": "High", - "description": "(Safety Impact:Hazardous AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Major AND Mission Impact:MEF Failure)" - }, - { - "key": "VH", - "name": "Very High", - "description": "Safety Impact:Catastrophic OR Mission Impact:Mission Failure" - } - ] -} \ No newline at end of file diff --git a/data/json/decision_points/public_safety_impact_1_0_0.json b/data/json/decision_points/public_safety_impact_1_0_0.json deleted file mode 100644 index 0426c72b..00000000 --- a/data/json/decision_points/public_safety_impact_1_0_0.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schemaVersion": "1-0-1", - "namespace": "ssvc", - "version": "1.0.0", - "key": "PSI", - "name": "Public Safety Impact", - "description": "A coarse-grained representation of impact to public safety.", - "values": [ - { - "key": "M", - "name": "Minimal", - "description": "Safety Impact:(None OR Minor)" - }, - { - "key": "S", - "name": "Significant", - "description": "Safety Impact:(Major OR Hazardous OR Catastrophic)" - } - ] -} \ No newline at end of file diff --git a/data/json/decision_points/automatable_2_0_0.json b/data/json/decision_points/ssvc/automatable_2_0_0.json similarity index 100% rename from data/json/decision_points/automatable_2_0_0.json rename to data/json/decision_points/ssvc/automatable_2_0_0.json diff --git a/data/json/decision_points/ssvc/critical_software_1_0_0.json b/data/json/decision_points/ssvc/critical_software_1_0_0.json new file mode 100644 index 00000000..232c633d --- /dev/null +++ b/data/json/decision_points/ssvc/critical_software_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "Critical Software", + "description": "Denotes whether a system meets a critical software definition.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "CS", + "values": [ + { + "key": "N", + "name": "No", + "description": "System does not meet a critical software definition." + }, + { + "key": "Y", + "name": "Yes", + "description": "System meets a critical software definition." + } + ] +} diff --git a/data/json/outcomes/COORDINATE.json b/data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json similarity index 86% rename from data/json/outcomes/COORDINATE.json rename to data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json index 67a4d9fa..fc7aae55 100644 --- a/data/json/outcomes/COORDINATE.json +++ b/data/json/decision_points/ssvc/decline_track_coordinate_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Decline, Track, Coordinate", "description": "The coordinate outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "COORDINATE", + "values": [ { "key": "D", "name": "Decline", @@ -20,4 +22,4 @@ "description": "Coordinate" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/DSOI.json b/data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json similarity index 90% rename from data/json/outcomes/DSOI.json rename to data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json index 8e16b6f6..2474d981 100644 --- a/data/json/outcomes/DSOI.json +++ b/data/json/decision_points/ssvc/defer_scheduled_out_of_cycle_immediate_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Defer, Scheduled, Out-of-Cycle, Immediate", "description": "The original SSVC outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "DSOI", + "values": [ { "key": "D", "name": "Defer", @@ -25,4 +27,4 @@ "description": "Immediate" } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/exploitation_1_0_0.json b/data/json/decision_points/ssvc/exploitation_1_0_0.json similarity index 100% rename from data/json/decision_points/exploitation_1_0_0.json rename to data/json/decision_points/ssvc/exploitation_1_0_0.json diff --git a/data/json/decision_points/exploitation_1_1_0.json b/data/json/decision_points/ssvc/exploitation_1_1_0.json similarity index 100% rename from data/json/decision_points/exploitation_1_1_0.json rename to data/json/decision_points/ssvc/exploitation_1_1_0.json diff --git a/data/json/decision_points/ssvc/high_value_asset_1_0_0.json b/data/json/decision_points/ssvc/high_value_asset_1_0_0.json new file mode 100644 index 00000000..a0e94922 --- /dev/null +++ b/data/json/decision_points/ssvc/high_value_asset_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "High Value Asset", + "description": "Denotes whether a system meets a high value asset definition.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "HVA", + "values": [ + { + "key": "N", + "name": "No", + "description": "System does not meet a high value asset definition." + }, + { + "key": "Y", + "name": "Yes", + "description": "System meets a high value asset definition." + } + ] +} diff --git a/data/json/decision_points/human_impact_2_0_0.json b/data/json/decision_points/ssvc/human_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/human_impact_2_0_0.json rename to data/json/decision_points/ssvc/human_impact_2_0_0.json diff --git a/data/json/decision_points/human_impact_2_0_1.json b/data/json/decision_points/ssvc/human_impact_2_0_1.json similarity index 100% rename from data/json/decision_points/human_impact_2_0_1.json rename to data/json/decision_points/ssvc/human_impact_2_0_1.json diff --git a/data/json/decision_points/ssvc/in_kev_1_0_0.json b/data/json/decision_points/ssvc/in_kev_1_0_0.json new file mode 100644 index 00000000..cadc960b --- /dev/null +++ b/data/json/decision_points/ssvc/in_kev_1_0_0.json @@ -0,0 +1,20 @@ +{ + "name": "In KEV", + "description": "Denotes whether a vulnerability is in the CISA Known Exploited Vulnerabilities (KEV) list.", + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "KEV", + "values": [ + { + "key": "N", + "name": "No", + "description": "Vulnerability is not listed in KEV." + }, + { + "key": "Y", + "name": "Yes", + "description": "Vulnerability is listed in KEV." + } + ] +} diff --git a/data/json/decision_points/mission_and_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/mission_and_well_being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/mission_and_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/mission_and_well_being_impact_1_0_0.json diff --git a/data/json/decision_points/mission_impact_1_0_0.json b/data/json/decision_points/ssvc/mission_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/mission_impact_1_0_0.json rename to data/json/decision_points/ssvc/mission_impact_1_0_0.json diff --git a/data/json/decision_points/mission_impact_2_0_0.json b/data/json/decision_points/ssvc/mission_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/mission_impact_2_0_0.json rename to data/json/decision_points/ssvc/mission_impact_2_0_0.json diff --git a/data/json/decision_points/public_safety_impact_2_0_0.json b/data/json/decision_points/ssvc/public_safety_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/public_safety_impact_2_0_0.json rename to data/json/decision_points/ssvc/public_safety_impact_2_0_0.json diff --git a/data/json/decision_points/public_safety_impact_2_0_1.json b/data/json/decision_points/ssvc/public_safety_impact_2_0_1.json similarity index 100% rename from data/json/decision_points/public_safety_impact_2_0_1.json rename to data/json/decision_points/ssvc/public_safety_impact_2_0_1.json diff --git a/data/json/decision_points/public_value_added_1_0_0.json b/data/json/decision_points/ssvc/public_value_added_1_0_0.json similarity index 100% rename from data/json/decision_points/public_value_added_1_0_0.json rename to data/json/decision_points/ssvc/public_value_added_1_0_0.json diff --git a/data/json/decision_points/public_well-being_impact_1_0_0.json b/data/json/decision_points/ssvc/public_well_being_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/public_well-being_impact_1_0_0.json rename to data/json/decision_points/ssvc/public_well_being_impact_1_0_0.json diff --git a/data/json/outcomes/PUBLISH.json b/data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json similarity index 84% rename from data/json/outcomes/PUBLISH.json rename to data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json index fd656624..aa5d01fc 100644 --- a/data/json/outcomes/PUBLISH.json +++ b/data/json/decision_points/ssvc/publish_do_not_publish_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Publish, Do Not Publish", "description": "The publish outcome group.", - "outcomes": [ + "namespace": "ssvc", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "PUBLISH", + "values": [ { "key": "N", "name": "Do Not Publish", @@ -15,4 +17,4 @@ "description": "Publish" } ] -} \ No newline at end of file +} diff --git a/data/json/decision_points/report_credibility_1_0_0.json b/data/json/decision_points/ssvc/report_credibility_1_0_0.json similarity index 100% rename from data/json/decision_points/report_credibility_1_0_0.json rename to data/json/decision_points/ssvc/report_credibility_1_0_0.json diff --git a/data/json/decision_points/report_public_1_0_0.json b/data/json/decision_points/ssvc/report_public_1_0_0.json similarity index 100% rename from data/json/decision_points/report_public_1_0_0.json rename to data/json/decision_points/ssvc/report_public_1_0_0.json diff --git a/data/json/decision_points/safety_impact_1_0_0.json b/data/json/decision_points/ssvc/safety_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/safety_impact_1_0_0.json rename to data/json/decision_points/ssvc/safety_impact_1_0_0.json diff --git a/data/json/decision_points/safety_impact_2_0_0.json b/data/json/decision_points/ssvc/safety_impact_2_0_0.json similarity index 100% rename from data/json/decision_points/safety_impact_2_0_0.json rename to data/json/decision_points/ssvc/safety_impact_2_0_0.json diff --git a/data/json/decision_points/supplier_cardinality_1_0_0.json b/data/json/decision_points/ssvc/supplier_cardinality_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_cardinality_1_0_0.json rename to data/json/decision_points/ssvc/supplier_cardinality_1_0_0.json diff --git a/data/json/decision_points/supplier_contacted_1_0_0.json b/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json similarity index 96% rename from data/json/decision_points/supplier_contacted_1_0_0.json rename to data/json/decision_points/ssvc/supplier_contacted_1_0_0.json index c32d5755..ee1cfb50 100644 --- a/data/json/decision_points/supplier_contacted_1_0_0.json +++ b/data/json/decision_points/ssvc/supplier_contacted_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "SC", + "key": "SCON", "values": [ { "key": "N", diff --git a/data/json/decision_points/supplier_engagement_1_0_0.json b/data/json/decision_points/ssvc/supplier_engagement_1_0_0.json similarity index 100% rename from data/json/decision_points/supplier_engagement_1_0_0.json rename to data/json/decision_points/ssvc/supplier_engagement_1_0_0.json diff --git a/data/json/decision_points/supplier_involvement_1_0_0.json b/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json similarity index 97% rename from data/json/decision_points/supplier_involvement_1_0_0.json rename to data/json/decision_points/ssvc/supplier_involvement_1_0_0.json index 15d014e5..b1e48b02 100644 --- a/data/json/decision_points/supplier_involvement_1_0_0.json +++ b/data/json/decision_points/ssvc/supplier_involvement_1_0_0.json @@ -4,7 +4,7 @@ "namespace": "ssvc", "version": "1.0.0", "schemaVersion": "1-0-1", - "key": "SI", + "key": "SINV", "values": [ { "key": "FR", diff --git a/data/json/decision_points/system_exposure_1_0_0.json b/data/json/decision_points/ssvc/system_exposure_1_0_0.json similarity index 100% rename from data/json/decision_points/system_exposure_1_0_0.json rename to data/json/decision_points/ssvc/system_exposure_1_0_0.json diff --git a/data/json/decision_points/system_exposure_1_0_1.json b/data/json/decision_points/ssvc/system_exposure_1_0_1.json similarity index 100% rename from data/json/decision_points/system_exposure_1_0_1.json rename to data/json/decision_points/ssvc/system_exposure_1_0_1.json diff --git a/data/json/decision_points/technical_impact_1_0_0.json b/data/json/decision_points/ssvc/technical_impact_1_0_0.json similarity index 100% rename from data/json/decision_points/technical_impact_1_0_0.json rename to data/json/decision_points/ssvc/technical_impact_1_0_0.json diff --git a/data/json/decision_points/utility_1_0_0.json b/data/json/decision_points/ssvc/utility_1_0_0.json similarity index 100% rename from data/json/decision_points/utility_1_0_0.json rename to data/json/decision_points/ssvc/utility_1_0_0.json diff --git a/data/json/decision_points/utility_1_0_1.json b/data/json/decision_points/ssvc/utility_1_0_1.json similarity index 100% rename from data/json/decision_points/utility_1_0_1.json rename to data/json/decision_points/ssvc/utility_1_0_1.json diff --git a/data/json/decision_points/value_density_1_0_0.json b/data/json/decision_points/ssvc/value_density_1_0_0.json similarity index 100% rename from data/json/decision_points/value_density_1_0_0.json rename to data/json/decision_points/ssvc/value_density_1_0_0.json diff --git a/data/json/decision_points/virulence_1_0_0.json b/data/json/decision_points/ssvc/virulence_1_0_0.json similarity index 100% rename from data/json/decision_points/virulence_1_0_0.json rename to data/json/decision_points/ssvc/virulence_1_0_0.json diff --git a/data/json/outcomes/EISENHOWER.json b/data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json similarity index 89% rename from data/json/outcomes/EISENHOWER.json rename to data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json index 40d98902..f83890fe 100644 --- a/data/json/outcomes/EISENHOWER.json +++ b/data/json/decision_points/x_basic/do_schedule_delegate_delete_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Do, Schedule, Delegate, Delete", "description": "The Eisenhower outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "IKE", + "values": [ { "key": "D", "name": "Delete", @@ -25,4 +27,4 @@ "description": "Do" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/MOSCOW.json b/data/json/decision_points/x_basic/moscow_1_0_0.json similarity index 71% rename from data/json/outcomes/MOSCOW.json rename to data/json/decision_points/x_basic/moscow_1_0_0.json index 3156c47d..77955737 100644 --- a/data/json/outcomes/MOSCOW.json +++ b/data/json/decision_points/x_basic/moscow_1_0_0.json @@ -1,9 +1,11 @@ { + "name": "MoSCoW", + "description": "The MoSCoW (Must, Should, Could, Won't) outcome group.", + "namespace": "x_basic", "version": "1.0.0", "schemaVersion": "1-0-1", - "name": "Must, Should, Could, Won't", - "description": "The Moscow outcome group.", - "outcomes": [ + "key": "MSCW", + "values": [ { "key": "W", "name": "Won't", @@ -25,4 +27,4 @@ "description": "Must" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/VALUE_COMPLEXITY.json b/data/json/decision_points/x_basic/value_complexity_1_0_0.json similarity index 87% rename from data/json/outcomes/VALUE_COMPLEXITY.json rename to data/json/decision_points/x_basic/value_complexity_1_0_0.json index b60d42f8..83579078 100644 --- a/data/json/outcomes/VALUE_COMPLEXITY.json +++ b/data/json/decision_points/x_basic/value_complexity_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Value, Complexity", "description": "The Value/Complexity outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "VALUE_COMPLEXITY", + "values": [ { "key": "D", "name": "Drop", @@ -25,4 +27,4 @@ "description": "Do First" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/YES_NO.json b/data/json/decision_points/x_basic/yes_no_1_0_0.json similarity index 82% rename from data/json/outcomes/YES_NO.json rename to data/json/decision_points/x_basic/yes_no_1_0_0.json index 1a6dcdff..6a3b9b23 100644 --- a/data/json/outcomes/YES_NO.json +++ b/data/json/decision_points/x_basic/yes_no_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "Yes, No", "description": "The Yes/No outcome group.", - "outcomes": [ + "namespace": "x_basic", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "YN", + "values": [ { "key": "N", "name": "No", @@ -15,4 +17,4 @@ "description": "Yes" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/THE_PARANOIDS.json b/data/json/decision_points/x_community/theparanoids_1_0_0.json similarity index 91% rename from data/json/outcomes/THE_PARANOIDS.json rename to data/json/decision_points/x_community/theparanoids_1_0_0.json index f19fb83d..82d8a4c5 100644 --- a/data/json/outcomes/THE_PARANOIDS.json +++ b/data/json/decision_points/x_community/theparanoids_1_0_0.json @@ -1,9 +1,11 @@ { - "version": "1.0.0", - "schemaVersion": "1-0-1", "name": "theParanoids", "description": "PrioritizedRiskRemediation outcome group based on TheParanoids.", - "outcomes": [ + "namespace": "x_community", + "version": "1.0.0", + "schemaVersion": "1-0-1", + "key": "PARANOIDS", + "values": [ { "key": "5", "name": "Track 5", @@ -35,4 +37,4 @@ "description": "Act ASAP" } ] -} \ No newline at end of file +} diff --git a/data/json/outcomes/CVSS.json b/data/json/outcomes/CVSS.json deleted file mode 100644 index 5d3d3bd2..00000000 --- a/data/json/outcomes/CVSS.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": "1.0.0", - "schemaVersion": "1-0-1", - "name": "CVSS Levels", - "description": "The CVSS outcome group.", - "outcomes": [ - { - "key": "L", - "name": "Low", - "description": "Low" - }, - { - "key": "M", - "name": "Medium", - "description": "Medium" - }, - { - "key": "H", - "name": "High", - "description": "High" - }, - { - "key": "C", - "name": "Critical", - "description": "Critical" - } - ] -} \ No newline at end of file diff --git a/docs/howto/coordination_triage_decision.md b/docs/howto/coordination_triage_decision.md index 6ad72cdc..53949660 100644 --- a/docs/howto/coordination_triage_decision.md +++ b/docs/howto/coordination_triage_decision.md @@ -82,13 +82,13 @@ The remaining five decision points are: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.report_public import LATEST as RP -from ssvc.decision_points.supplier_contacted import LATEST as SC -from ssvc.decision_points.report_credibility import LATEST as RC -from ssvc.decision_points.supplier_cardinality import LATEST as SI -from ssvc.decision_points.supplier_engagement import LATEST as SE -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc.report_public import LATEST as RP +from ssvc.decision_points.ssvc.supplier_contacted import LATEST as SC +from ssvc.decision_points.ssvc.report_credibility import LATEST as RC +from ssvc.decision_points.ssvc.supplier_cardinality import LATEST as SI +from ssvc.decision_points.ssvc.supplier_engagement import LATEST as SE +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block for dp in [RP, SC, RC, SI, SE, U, PSI]: diff --git a/docs/howto/deployer_tree.md b/docs/howto/deployer_tree.md index 961a475e..ffbde896 100644 --- a/docs/howto/deployer_tree.md +++ b/docs/howto/deployer_tree.md @@ -113,10 +113,10 @@ The Deployer Patch Deployment Priority decision model uses the following decisio More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.system_exposure import LATEST as SE -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.human_impact import LATEST as HI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.system_exposure import LATEST as SE +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.human_impact import LATEST as HI from ssvc.doc_helpers import example_block for dp in [EXP, SE, U, HI]: diff --git a/docs/howto/publication_decision.md b/docs/howto/publication_decision.md index 1673302a..739b3b88 100644 --- a/docs/howto/publication_decision.md +++ b/docs/howto/publication_decision.md @@ -133,9 +133,9 @@ and adds two new ones ([*Supplier Involvement*](../reference/decision_points/sup More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_involvement import LATEST as SI -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.public_value_added import LATEST as PVA +from ssvc.decision_points.ssvc.supplier_involvement import LATEST as SI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.public_value_added import LATEST as PVA from ssvc.doc_helpers import example_block diff --git a/docs/howto/supplier_tree.md b/docs/howto/supplier_tree.md index e714e907..3d1c2384 100644 --- a/docs/howto/supplier_tree.md +++ b/docs/howto/supplier_tree.md @@ -72,10 +72,10 @@ The decision to create a patch is based on the following decision points: More detail about each of these decision points is provided at the links above, here we provide a brief summary of each. ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST as EXP -from ssvc.decision_points.utility import LATEST as U -from ssvc.decision_points.technical_impact import LATEST as TI -from ssvc.decision_points.public_safety_impact import LATEST as PSI +from ssvc.decision_points.ssvc.exploitation import LATEST as EXP +from ssvc.decision_points.ssvc.utility import LATEST as U +from ssvc.decision_points.ssvc.technical_impact import LATEST as TI +from ssvc.decision_points.ssvc.public_safety_impact import LATEST as PSI from ssvc.doc_helpers import example_block diff --git a/docs/reference/decision_points/automatable.md b/docs/reference/decision_points/automatable.md index 69259cfa..33592885 100644 --- a/docs/reference/decision_points/automatable.md +++ b/docs/reference/decision_points/automatable.md @@ -1,7 +1,7 @@ # Automatable (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.automatable import LATEST +from ssvc.decision_points.ssvc.automatable import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -62,7 +62,7 @@ Due to vulnerability chaining, there is some nuance as to whether reconnaissance ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.automatable import VERSIONS +from ssvc.decision_points.ssvc.automatable import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/exploitation.md b/docs/reference/decision_points/exploitation.md index b4f93bb3..b0c92a43 100644 --- a/docs/reference/decision_points/exploitation.md +++ b/docs/reference/decision_points/exploitation.md @@ -1,7 +1,7 @@ # Exploitation ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import LATEST +from ssvc.decision_points.ssvc.exploitation import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -49,7 +49,7 @@ The table below lists CWE-IDs that could be used to mark a vulnerability as *PoC ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.exploitation import VERSIONS +from ssvc.decision_points.ssvc.exploitation import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/human_impact.md b/docs/reference/decision_points/human_impact.md index 04057d11..339cdceb 100644 --- a/docs/reference/decision_points/human_impact.md +++ b/docs/reference/decision_points/human_impact.md @@ -1,7 +1,7 @@ # Human Impact ```python exec="true" idprefix="" -from ssvc.decision_points.human_impact import LATEST +from ssvc.decision_points.ssvc.human_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -47,7 +47,7 @@ see [Guidance on Communicating Results](../../howto/bootstrap/use.md). ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.human_impact import VERSIONS +from ssvc.decision_points.ssvc.human_impact import VERSIONS from ssvc.doc_helpers import prior_version, example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/mission_impact.md b/docs/reference/decision_points/mission_impact.md index f8b80503..801aea6b 100644 --- a/docs/reference/decision_points/mission_impact.md +++ b/docs/reference/decision_points/mission_impact.md @@ -1,7 +1,7 @@ # Mission Impact ```python exec="true" idprefix="" -from ssvc.decision_points.mission_impact import LATEST +from ssvc.decision_points.ssvc.mission_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -42,7 +42,7 @@ It should require the vulnerability management team to interact with more senior ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.mission_impact import VERSIONS +from ssvc.decision_points.ssvc.mission_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_safety_impact.md b/docs/reference/decision_points/public_safety_impact.md index 44b774b0..15960fa5 100644 --- a/docs/reference/decision_points/public_safety_impact.md +++ b/docs/reference/decision_points/public_safety_impact.md @@ -1,7 +1,7 @@ # Public Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.public_safety_impact import LATEST +from ssvc.decision_points.ssvc.public_safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -21,7 +21,7 @@ Therefore we simplify the above into a binary categorization: ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.public_safety_impact import VERSIONS +from ssvc.decision_points.ssvc.public_safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/public_value_added.md b/docs/reference/decision_points/public_value_added.md index 0284c0da..8a07a42e 100644 --- a/docs/reference/decision_points/public_value_added.md +++ b/docs/reference/decision_points/public_value_added.md @@ -1,7 +1,7 @@ # Public Value Added ```python exec="true" idprefix="" -from ssvc.decision_points.public_value_added import LATEST +from ssvc.decision_points.ssvc.public_value_added import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_credibility.md b/docs/reference/decision_points/report_credibility.md index acce744c..0bd2047c 100644 --- a/docs/reference/decision_points/report_credibility.md +++ b/docs/reference/decision_points/report_credibility.md @@ -1,7 +1,7 @@ # Report Credibility ```python exec="true" idprefix="" -from ssvc.decision_points.report_credibility import LATEST +from ssvc.decision_points.ssvc.report_credibility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/report_public.md b/docs/reference/decision_points/report_public.md index aa795f2e..fd5df8e2 100644 --- a/docs/reference/decision_points/report_public.md +++ b/docs/reference/decision_points/report_public.md @@ -1,7 +1,7 @@ # Report Public ```python exec="true" idprefix="" -from ssvc.decision_points.report_public import LATEST +from ssvc.decision_points.ssvc.report_public import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/safety_impact.md b/docs/reference/decision_points/safety_impact.md index 2c9418c4..128275ba 100644 --- a/docs/reference/decision_points/safety_impact.md +++ b/docs/reference/decision_points/safety_impact.md @@ -1,7 +1,7 @@ # Safety Impact ```python exec="true" idprefix="" -from ssvc.decision_points.safety_impact import LATEST +from ssvc.decision_points.ssvc.safety_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -217,7 +217,7 @@ We defer this topic for now because we combine it with [*Mission Impact*](missio ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.safety_impact import VERSIONS +from ssvc.decision_points.ssvc.safety_impact import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/supplier_cardinality.md b/docs/reference/decision_points/supplier_cardinality.md index ccd088fa..6c27a0f3 100644 --- a/docs/reference/decision_points/supplier_cardinality.md +++ b/docs/reference/decision_points/supplier_cardinality.md @@ -1,7 +1,7 @@ # Supplier Cardinality ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_cardinality import LATEST +from ssvc.decision_points.ssvc.supplier_cardinality import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_contacted.md b/docs/reference/decision_points/supplier_contacted.md index f75e1615..d8bcbdcf 100644 --- a/docs/reference/decision_points/supplier_contacted.md +++ b/docs/reference/decision_points/supplier_contacted.md @@ -1,7 +1,7 @@ # Supplier Contacted ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_contacted import LATEST +from ssvc.decision_points.ssvc.supplier_contacted import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_engagement.md b/docs/reference/decision_points/supplier_engagement.md index c8a7426b..35b395d7 100644 --- a/docs/reference/decision_points/supplier_engagement.md +++ b/docs/reference/decision_points/supplier_engagement.md @@ -1,7 +1,7 @@ # Supplier Engagement ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_engagement import LATEST +from ssvc.decision_points.ssvc.supplier_engagement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/supplier_involvement.md b/docs/reference/decision_points/supplier_involvement.md index d4fb9d70..519db57b 100644 --- a/docs/reference/decision_points/supplier_involvement.md +++ b/docs/reference/decision_points/supplier_involvement.md @@ -1,7 +1,7 @@ # Supplier Involvement ```python exec="true" idprefix="" -from ssvc.decision_points.supplier_involvement import LATEST +from ssvc.decision_points.ssvc.supplier_involvement import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/system_exposure.md b/docs/reference/decision_points/system_exposure.md index 9a2f52dd..5bbaa2f1 100644 --- a/docs/reference/decision_points/system_exposure.md +++ b/docs/reference/decision_points/system_exposure.md @@ -1,7 +1,7 @@ # System Exposure ```python exec="true" idprefix="" -from ssvc.decision_points.system_exposure import LATEST +from ssvc.decision_points.ssvc.system_exposure import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -44,7 +44,7 @@ If you have suggestions for further heuristics, or potential counterexamples to ## Prior Versions ```python exec="true" idprefix="" -from ssvc.decision_points.system_exposure import VERSIONS +from ssvc.decision_points.ssvc.system_exposure import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/technical_impact.md b/docs/reference/decision_points/technical_impact.md index 4b1dcaf6..0c3925ff 100644 --- a/docs/reference/decision_points/technical_impact.md +++ b/docs/reference/decision_points/technical_impact.md @@ -1,7 +1,7 @@ # Technical Impact ```python exec="true" idprefix="" -from ssvc.decision_points.technical_impact import LATEST +from ssvc.decision_points.ssvc.technical_impact import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/docs/reference/decision_points/utility.md b/docs/reference/decision_points/utility.md index 1c465d41..13fca8b1 100644 --- a/docs/reference/decision_points/utility.md +++ b/docs/reference/decision_points/utility.md @@ -1,7 +1,7 @@ # Utility ```python exec="true" idprefix="" -from ssvc.decision_points.utility import LATEST +from ssvc.decision_points.ssvc.utility import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) @@ -46,7 +46,7 @@ However, future work should look for and prevent large mismatches between the ou ## Previous Versions ```python exec="true" idprefix="" -from ssvc.decision_points.utility import VERSIONS +from ssvc.decision_points.ssvc.utility import VERSIONS from ssvc.doc_helpers import example_block versions = VERSIONS[:-1] diff --git a/docs/reference/decision_points/value_density.md b/docs/reference/decision_points/value_density.md index 11b02a3b..265c680c 100644 --- a/docs/reference/decision_points/value_density.md +++ b/docs/reference/decision_points/value_density.md @@ -1,7 +1,7 @@ # Value Density (SSVC) ```python exec="true" idprefix="" -from ssvc.decision_points.value_density import LATEST +from ssvc.decision_points.ssvc.value_density import LATEST from ssvc.doc_helpers import example_block print(example_block(LATEST)) diff --git a/src/ssvc/_mixins.py b/src/ssvc/_mixins.py index 4e52ff35..67576910 100644 --- a/src/ssvc/_mixins.py +++ b/src/ssvc/_mixins.py @@ -26,8 +26,8 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator from semver import Version +from ssvc import _schemaVersion from ssvc.namespaces import NS_PATTERN, NameSpace -from . import _schemaVersion class _Versioned(BaseModel): @@ -36,7 +36,6 @@ class _Versioned(BaseModel): """ version: str = "0.0.0" - schemaVersion: str = _schemaVersion @field_validator("version") @classmethod @@ -56,6 +55,14 @@ def validate_version(cls, value: str) -> str: return version.__str__() +class _SchemaVersioned(_Versioned, BaseModel): + """ + Mixin class for version + """ + + schemaVersion: str = _schemaVersion + + class _Namespaced(BaseModel): """ Mixin class for namespaced SSVC objects. diff --git a/src/ssvc/csv_analyzer.py b/src/ssvc/csv_analyzer.py index 13ccdcd0..890dad78 100644 --- a/src/ssvc/csv_analyzer.py +++ b/src/ssvc/csv_analyzer.py @@ -73,6 +73,9 @@ logger = logging.getLogger(__name__) +# set an option to avoid a deprecation warning +pd.set_option("future.no_silent_downcasting", True) + def _col_norm(c: str) -> str: """ @@ -101,9 +104,7 @@ def _imp_df(column_names: list, importances: list) -> pd.DataFrame: a dataframe of feature importances """ df = ( - pd.DataFrame( - {"feature": column_names, "feature_importance": importances} - ) + pd.DataFrame({"feature": column_names, "feature_importance": importances}) .sort_values("feature_importance", ascending=False) .reset_index(drop=True) ) @@ -192,9 +193,7 @@ def _perm_feat_imp(model, x, y): def _parse_args(args) -> argparse.Namespace: # parse command line - parser = argparse.ArgumentParser( - description="Analyze an SSVC tree csv file" - ) + parser = argparse.ArgumentParser(description="Analyze an SSVC tree csv file") parser.add_argument( "csvfile", metavar="csvfile", type=str, help="the csv file to analyze" ) @@ -381,12 +380,8 @@ def check_topological_order(df, target): for u in H.nodes: H.nodes[u]["outcome"] = G.nodes[u]["outcome"] - logger.debug( - f"Original graph: {len(G.nodes)} nodes with {len(G.edges)} edges" - ) - logger.debug( - f"Reduced graph: {len(H.nodes)} nodes with {len(H.edges)} edges" - ) + logger.debug(f"Original graph: {len(G.nodes)} nodes with {len(G.edges)} edges") + logger.debug(f"Reduced graph: {len(H.nodes)} nodes with {len(H.edges)} edges") problems = [] # check if the outcome is topologically sorted diff --git a/src/ssvc/decision_points/__init__.py b/src/ssvc/decision_points/__init__.py index cfcb327a..7bd86f6d 100644 --- a/src/ssvc/decision_points/__init__.py +++ b/src/ssvc/decision_points/__init__.py @@ -1,22 +1,5 @@ #!/usr/bin/env python -""" -The ssvc.decision_points package provides a set of decision points for use in SSVC decision functions. - -Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a -single aspect of the input to a decision function. Decision points should have the following characteristics: - -- A name or label -- A description -- A version (a semantic version string) -- A namespace (a short, unique string): For example, "ssvc" or "cvss" to indicate the source of the decision point -- A key (a short, unique string) that can be used to identify the decision point in a shorthand way -- A short enumeration of possible values -In turn, each value should have the following characteristics: -- A name or label -- A description -- A key (a short, unique string) that can be used to identify the value in a shorthand way -""" # Copyright (c) 2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE # ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. @@ -36,4 +19,8 @@ # subject to its own license. # DM24-0278 -from .base import SsvcDecisionPoint, SsvcDecisionPointValue +""" +The ssvc.decision_points package provides a set of decision points for use in SSVC decision functions. +Decision points are the basic building blocks of SSVC decision functions. Individual decision points describe a +single aspect of the input to a decision function. +""" diff --git a/src/ssvc/decision_points/base.py b/src/ssvc/decision_points/base.py index 1bad79bc..69e42d96 100644 --- a/src/ssvc/decision_points/base.py +++ b/src/ssvc/decision_points/base.py @@ -24,30 +24,89 @@ import logging -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field, model_validator -from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned -from ssvc.namespaces import NameSpace +from ssvc._mixins import ( + _Base, + _Commented, + _Keyed, + _Namespaced, + _SchemaVersioned, + _Valued, + _Versioned, +) logger = logging.getLogger(__name__) -_RDP = {} REGISTERED_DECISION_POINTS = [] +FIELD_DELIMITER = ":" + + +class Registry(BaseModel): + registry: dict[str, object] = Field(default_factory=dict) + + def __iter__(self) -> object: + return iter(self.registry.values()) + + def __getitem__(self, key: str) -> object: + return self.registry[key] + + def __setitem__(self, key: str, value: object) -> None: + + if key in self.registry: + # are the values the same? + registered = self.registry[key].model_dump_json() + value_dumped = value.model_dump_json() + if registered == value_dumped: + logger.warning(f"Duplicate key {key} with the same value, ignoring.") + return + + logger.warning(f"Duplicate key {key}:") + logger.warning(f"\t{registered}") + logger.warning(f"\t{value_dumped}") + raise KeyError(f"Duplicate key {key}") + + self.registry[key] = value + + def __contains__(self, key: str) -> bool: + return key in self.registry + + def reset_registry(self) -> None: + self.registry = {} + + # convenience alias + def clear(self) -> None: + self.reset_registry() + + +class DecisionPointRegistry(Registry, BaseModel): + """ + A dictionary of decision points. + """ + + registry: dict[str, "DecisionPoint"] = Field(default_factory=dict) + + +class DecisionPointValueRegistry(Registry, BaseModel): + """ + A dictionary of decision point values. + """ + + registry: dict[str, "DecisionPointValue"] = Field(default_factory=dict) def register(dp): """ Register a decision point. """ - global _RDP - - key = (dp.namespace, dp.name, dp.key, dp.version) - if key in _RDP: - logger.warning(f"Duplicate decision point {key}") + # register the values + for value_str, value_summary in dp.value_summaries_dict.items(): + DPV_REGISTRY[value_str] = value_summary - _RDP[key] = dp + key = dp.str + DP_REGISTRY[key] = dp REGISTERED_DECISION_POINTS.append(dp) @@ -55,54 +114,159 @@ def _reset_registered(): """ Reset the registered decision points. """ - global _RDP + global DPV_REGISTRY + global DP_REGISTRY global REGISTERED_DECISION_POINTS - _RDP = {} + DPV_REGISTRY.reset_registry() + DP_REGISTRY.reset_registry() REGISTERED_DECISION_POINTS = [] -class SsvcDecisionPointValue(_Base, _Keyed, BaseModel): +class DecisionPointValue(_Base, _Keyed, _Commented, BaseModel): """ Models a single value option for a decision point. + + Each value should have the following attributes: + + - name (str): A name + - description (str): A description + - key (str): A key (a short, unique string) that can be used to identify the value in a shorthand way + - _comment (str): An optional comment that will be included in the object. """ + def __str__(self): + return self.name -class SsvcDecisionPoint(_Valued, _Keyed, _Versioned, _Namespaced, _Base, BaseModel): + +class ValueSummary(_Versioned, _Keyed, _Namespaced, BaseModel): + """ + A ValueSummary is a simple object that represents a single value for a decision point. + It includes the parent decision point's key, version, namespace, and the value key. + These can be used to reference a specific value in a decision point. + """ + + value: str + + def __str__(self): + s = FIELD_DELIMITER.join([self.namespace, self.key, self.version, self.value]) + return s + + @property + def str(self): + """ + Return the ValueSummary as a string. + + Returns: + str: A string representation of the ValueSummary, in the format "namespace:key:version:value". + + """ + return self.__str__() + + +class DecisionPoint( + _Valued, _Keyed, _SchemaVersioned, _Namespaced, _Base, _Commented, BaseModel +): """ Models a single decision point as a list of values. + + Decision points should have the following attributes: + + - name (str): The name of the decision point + - description (str): A description of the decision point + - version (str): A semantic version string for the decision point + - namespace (str): The namespace (a short, unique string): For example, "ssvc" or "cvss" to indicate the source of the decision point + - key (str): A key (a short, unique string within the namespace) that can be used to identify the decision point in a shorthand way + - values (tuple): A tuple of DecisionPointValue objects """ - namespace: str = NameSpace.SSVC - values: tuple[SsvcDecisionPointValue, ...] + values: tuple[DecisionPointValue, ...] + + model_config = ConfigDict(revalidate_instances="always") - def __iter__(self): + def __str__(self): + return FIELD_DELIMITER.join([self.namespace, self.key, self.version]) + + @property + def str(self) -> str: """ - Allow iteration over the decision points in the group. + Return the DecisionPoint represented as a short string. + + Returns: + str: A string representation of the DecisionPoint, in the format "namespace:key:version". + """ - return iter(self.values) + return self.__str__() - def __init__(self, **data): - super().__init__(**data) + @model_validator(mode="after") + def _register(self): + """ + Register the decision point. + """ register(self) + return self - def __post_init__(self): - register(self) + @property + def value_summaries(self) -> list[ValueSummary]: + """ + Return a list of value summaries. + """ + return list(self.value_summaries_dict.values()) + + @property + def value_summaries_dict(self) -> dict[str, ValueSummary]: + """ + Return a dictionary of value summaries keyed by the value key. + """ + summaries = {} + for value in self.values: + summary = ValueSummary( + key=self.key, + version=self.version, + namespace=self.namespace, + value=value.key, + ) + key = summary.str + summaries[key] = summary + + return summaries + + @property + def value_summaries_str(self): + """ + Return a list of value summaries as strings. + + Returns: + list: A list of strings, each representing a value summary in the format "namespace:key:version:value". + + """ + return list(self.value_summaries_dict.keys()) + + @property + def enumerated_values(self) -> dict[int, str]: + """ + Return a list of enumerated values. + """ + return {i: v.str for i, v in enumerate(self.value_summaries)} + + +DP_REGISTRY = DecisionPointRegistry() +DPV_REGISTRY = DecisionPointRegistry() def main(): - opt_none = SsvcDecisionPointValue( + opt_none = DecisionPointValue( name="None", key="N", description="No exploit available" ) - opt_poc = SsvcDecisionPointValue( + opt_poc = DecisionPointValue( name="PoC", key="P", description="Proof of concept exploit available" ) - opt_active = SsvcDecisionPointValue( + opt_active = DecisionPointValue( name="Active", key="A", description="Active exploitation observed" ) opts = [opt_none, opt_poc, opt_active] - dp = SsvcDecisionPoint( + dp = DecisionPoint( _comment="This is an optional comment that will be included in the object.", values=opts, name="Exploitation", diff --git a/src/ssvc/decision_points/cisa/__init__.py b/src/ssvc/decision_points/cisa/__init__.py new file mode 100644 index 00000000..e9f8bc40 --- /dev/null +++ b/src/ssvc/decision_points/cisa/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides Decision Point objects in the `cisa` namespace +""" diff --git a/src/ssvc/decision_points/cisa/base.py b/src/ssvc/decision_points/cisa/base.py new file mode 100644 index 00000000..342c37a9 --- /dev/null +++ b/src/ssvc/decision_points/cisa/base.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +""" +Provides a base class for CISA-specific decision points. +""" +from pydantic import BaseModel + +from ssvc.decision_points.base import DecisionPoint +from ssvc.namespaces import NameSpace + + +class CisaDecisionPoint(DecisionPoint, BaseModel): + namespace: str = NameSpace.CISA diff --git a/src/ssvc/decision_points/mission_prevalence.py b/src/ssvc/decision_points/cisa/mission_prevalence.py similarity index 89% rename from src/ssvc/decision_points/mission_prevalence.py rename to src/ssvc/decision_points/cisa/mission_prevalence.py index e22d9fd0..9e209c9e 100644 --- a/src/ssvc/decision_points/mission_prevalence.py +++ b/src/ssvc/decision_points/cisa/mission_prevalence.py @@ -23,30 +23,32 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue + +from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -MINIMAL = SsvcDecisionPointValue( +MINIMAL = DecisionPointValue( name="Minimal", key="M", description="Neither Support nor Essential apply. " "The vulnerable component may be used within the entities, but it is not used as a mission-essential component, nor does it provide impactful support to mission-essential functions.", ) -SUPPORT = SsvcDecisionPointValue( +SUPPORT = DecisionPointValue( name="Support", key="S", description="The vulnerable component only supports MEFs for two or more entities.", ) -ESSENTIAL = SsvcDecisionPointValue( +ESSENTIAL = DecisionPointValue( name="Essential", key="E", description="The vulnerable component directly provides capabilities that constitute at least one MEF for at least one entity; component failure may (but does not necessarily) lead to overall mission failure.", ) -MISSION_PREVALENCE = SsvcDecisionPoint( +MISSION_PREVALENCE = CisaDecisionPoint( name="Mission Prevalence", description="Prevalence of the mission essential functions", key="MP", diff --git a/src/ssvc/decision_points/cvss/_not_defined.py b/src/ssvc/decision_points/cvss/_not_defined.py index dbb7164a..479c575b 100644 --- a/src/ssvc/decision_points/cvss/_not_defined.py +++ b/src/ssvc/decision_points/cvss/_not_defined.py @@ -21,16 +21,16 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue -NOT_DEFINED_ND = SsvcDecisionPointValue( +NOT_DEFINED_ND = DecisionPointValue( name="Not Defined", key="ND", description="This metric value is not defined. See CVSS documentation for details.", ) -NOT_DEFINED_X = SsvcDecisionPointValue( +NOT_DEFINED_X = DecisionPointValue( name="Not Defined", key="X", description="This metric value is not defined. See CVSS documentation for details.", diff --git a/src/ssvc/decision_points/cvss/attack_complexity.py b/src/ssvc/decision_points/cvss/attack_complexity.py index d2c56d78..f7aecd03 100644 --- a/src/ssvc/decision_points/cvss/attack_complexity.py +++ b/src/ssvc/decision_points/cvss/attack_complexity.py @@ -21,17 +21,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH_3 = SsvcDecisionPointValue( +_HIGH_3 = DecisionPointValue( name="High", key="H", description="A successful attack depends on conditions beyond the attacker's control.", ) -_LOW_3 = SsvcDecisionPointValue( +_LOW_3 = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist. An attacker can expect " @@ -39,20 +39,20 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Specialized access conditions exist." ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="The access conditions are somewhat specialized.", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Specialized access conditions exist; for example: the system is exploitable during specific windows " @@ -60,7 +60,7 @@ "configurations), or the system is exploitable with victim interaction (vulnerability exploitable " "only if user opens e-mail)", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Specialized access conditions or extenuating circumstances do not exist; the system is always " @@ -106,7 +106,7 @@ Defines LOW and HIGH values for CVSS Attack Complexity. """ -LOW_4 = SsvcDecisionPointValue( +LOW_4 = DecisionPointValue( name="Low", key="L", description="The attacker must take no measurable action to exploit the vulnerability. The attack requires no " @@ -114,7 +114,7 @@ "success against the vulnerable system. ", ) -HIGH_4 = SsvcDecisionPointValue( +HIGH_4 = DecisionPointValue( name="High", key="H", description="The successful attack depends on the evasion or circumvention of security-enhancing " diff --git a/src/ssvc/decision_points/cvss/attack_requirements.py b/src/ssvc/decision_points/cvss/attack_requirements.py index 37bc58cb..725ea7a9 100644 --- a/src/ssvc/decision_points/cvss/attack_requirements.py +++ b/src/ssvc/decision_points/cvss/attack_requirements.py @@ -21,11 +21,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AT_NONE = SsvcDecisionPointValue( +_AT_NONE = DecisionPointValue( name="None", key="N", description="The successful attack does not depend on the deployment and execution conditions of the vulnerable " @@ -34,7 +34,7 @@ ) -_PRESENT = SsvcDecisionPointValue( +_PRESENT = DecisionPointValue( name="Present", key="P", description="The successful attack depends on the presence of specific deployment and execution conditions of " diff --git a/src/ssvc/decision_points/cvss/attack_vector.py b/src/ssvc/decision_points/cvss/attack_vector.py index abd7c4a4..9bdb1e34 100644 --- a/src/ssvc/decision_points/cvss/attack_vector.py +++ b/src/ssvc/decision_points/cvss/attack_vector.py @@ -21,17 +21,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_REMOTE = SsvcDecisionPointValue( +_REMOTE = DecisionPointValue( name="Remote", key="R", description="The vulnerability is exploitable remotely.", ) -_LOCAL = SsvcDecisionPointValue( +_LOCAL = DecisionPointValue( name="Local", key="L", description="The vulnerability is only exploitable locally (i.e., it requires physical access or authenticated " @@ -52,7 +52,7 @@ Defines LOCAL and REMOTE values for CVSS Access Vector. """ -_NETWORK = SsvcDecisionPointValue( +_NETWORK = DecisionPointValue( name="Network", key="N", description="A vulnerability exploitable with network access means the vulnerable software is bound to the " @@ -60,14 +60,14 @@ "vulnerability is often termed 'remotely exploitable'.", ) -_ADJACENT = SsvcDecisionPointValue( +_ADJACENT = DecisionPointValue( name="Adjacent Network", key="A", description="A vulnerability exploitable with adjacent network access requires the attacker to have access to " "either the broadcast or collision domain of the vulnerable software.", ) -_LOCAL_2 = SsvcDecisionPointValue( +_LOCAL_2 = DecisionPointValue( name="Local", key="L", description="A vulnerability exploitable with only local access requires the attacker to have either physical " @@ -91,7 +91,7 @@ """ -_NETWORK_2 = SsvcDecisionPointValue( +_NETWORK_2 = DecisionPointValue( name="Network", key="N", description="A vulnerability exploitable with network access means the vulnerable component is bound to the " @@ -100,7 +100,7 @@ "exploitable one or more network hops away (e.g. across layer 3 boundaries from routers).", ) -_ADJACENT_2 = SsvcDecisionPointValue( +_ADJACENT_2 = DecisionPointValue( name="Adjacent", key="A", description="A vulnerability exploitable with adjacent network access means the vulnerable component is bound to " @@ -109,7 +109,7 @@ "3 boundary (e.g. a router).", ) -_LOCAL_3 = SsvcDecisionPointValue( +_LOCAL_3 = DecisionPointValue( name="Local", key="L", description="A vulnerability exploitable with Local access means that the vulnerable component is not bound to " @@ -118,7 +118,7 @@ "on User Interaction to execute a malicious file.", ) -_PHYSICAL_2 = SsvcDecisionPointValue( +_PHYSICAL_2 = DecisionPointValue( name="Physical", key="P", description="A vulnerability exploitable with Physical access requires the attacker to physically touch or " @@ -144,7 +144,7 @@ # CVSS v4 Attack Vector -_NETWORK_3 = SsvcDecisionPointValue( +_NETWORK_3 = DecisionPointValue( name="Network", key="N", description="The vulnerable system is bound to the network stack and the set of possible attackers extends beyond " @@ -153,7 +153,7 @@ "protocol level one or more network hops away (e.g., across one or more routers).", ) -_ADJACENT_3 = SsvcDecisionPointValue( +_ADJACENT_3 = DecisionPointValue( name="Adjacent", key="A", description="The vulnerable system is bound to a protocol stack, but the attack is limited at the protocol level " @@ -163,7 +163,7 @@ "administrative network zone).", ) -_LOCAL_4 = SsvcDecisionPointValue( +_LOCAL_4 = DecisionPointValue( name="Local", key="L", description="The vulnerable system is not bound to the network stack and the attacker’s path is via " @@ -174,7 +174,7 @@ "malicious document).", ) -_PHYSICAL_3 = SsvcDecisionPointValue( +_PHYSICAL_3 = DecisionPointValue( name="Physical", key="P", description="The attack requires the attacker to physically touch or manipulate the vulnerable system. Physical " diff --git a/src/ssvc/decision_points/cvss/authentication.py b/src/ssvc/decision_points/cvss/authentication.py index 5bf9a0cc..5f8eae2d 100644 --- a/src/ssvc/decision_points/cvss/authentication.py +++ b/src/ssvc/decision_points/cvss/authentication.py @@ -22,35 +22,35 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AUTH_NONE = SsvcDecisionPointValue( +_AUTH_NONE = DecisionPointValue( name="None", key="N", description="Authentication is not required to exploit the vulnerability.", ) -_SINGLE = SsvcDecisionPointValue( +_SINGLE = DecisionPointValue( name="Single", key="S", description="The vulnerability requires an attacker to be logged into the system (such as at a command line or via a desktop session or web interface).", ) -_MULTIPLE = SsvcDecisionPointValue( +_MULTIPLE = DecisionPointValue( name="Multiple", key="M", description="Exploiting the vulnerability requires that the attacker authenticate two or more times, even if the same credentials are used each time.", ) -_REQUIRED = SsvcDecisionPointValue( +_REQUIRED = DecisionPointValue( name="Required", key="R", description="Authentication is required to access and exploit the vulnerability.", ) -_NOT_REQUIRED = SsvcDecisionPointValue( +_NOT_REQUIRED = DecisionPointValue( name="Not Required", key="N", description="Authentication is not required to access or exploit the vulnerability.", diff --git a/src/ssvc/decision_points/cvss/availability_impact.py b/src/ssvc/decision_points/cvss/availability_impact.py index 17b0cd16..b7c5ed33 100644 --- a/src/ssvc/decision_points/cvss/availability_impact.py +++ b/src/ssvc/decision_points/cvss/availability_impact.py @@ -22,11 +22,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="There is total loss of availability, resulting in the attacker being able to fully deny access to " @@ -34,25 +34,25 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="There is reduced performance or interruptions in resource availability.", ) -_NONE_2 = SsvcDecisionPointValue( +_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no impact to the availability of the system.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="Total shutdown of the affected resource. The attacker can render the resource completely unavailable.", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="Considerable lag in or interruptions in resource availability. For example, a network-based flood " @@ -60,7 +60,7 @@ "number of connections successfully complete.", ) -_NONE_1 = SsvcDecisionPointValue( +_NONE_1 = DecisionPointValue( name="None", key="N", description="No impact on availability." ) @@ -95,7 +95,7 @@ Updates None. Removes Partial and Complete. Adds Low and High values for CVSS Availability Impact. """ -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="There is total loss of availability, resulting in the attacker being able to fully deny access to " @@ -103,7 +103,7 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="There is reduced performance or interruptions in resource availability. Even if repeated " @@ -113,7 +113,7 @@ "serious consequence to the Vulnerable System.", ) -_NONE_3 = SsvcDecisionPointValue( +_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no impact to availability within the Vulnerable System.", diff --git a/src/ssvc/decision_points/cvss/availability_requirement.py b/src/ssvc/decision_points/cvss/availability_requirement.py index 0f823bbd..a954a96a 100644 --- a/src/ssvc/decision_points/cvss/availability_requirement.py +++ b/src/ssvc/decision_points/cvss/availability_requirement.py @@ -22,7 +22,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -31,21 +31,21 @@ from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of availability is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of availability is likely to have a serious adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of availability is likely to have only a limited adverse effect on the organization or " @@ -83,21 +83,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of availability is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of availability is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of availability is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/base.py b/src/ssvc/decision_points/cvss/base.py index 3b881c33..40436b27 100644 --- a/src/ssvc/decision_points/cvss/base.py +++ b/src/ssvc/decision_points/cvss/base.py @@ -23,11 +23,11 @@ from pydantic import BaseModel -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPoint from ssvc.namespaces import NameSpace -class CvssDecisionPoint(SsvcDecisionPoint, BaseModel): +class CvssDecisionPoint(DecisionPoint, BaseModel): """ Models a single CVSS decision point as a list of values. """ diff --git a/src/ssvc/decision_points/cvss/collateral_damage_potential.py b/src/ssvc/decision_points/cvss/collateral_damage_potential.py index 7db822aa..3f4eb34c 100644 --- a/src/ssvc/decision_points/cvss/collateral_damage_potential.py +++ b/src/ssvc/decision_points/cvss/collateral_damage_potential.py @@ -22,49 +22,49 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_ND from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_MEDIUM_HIGH = SsvcDecisionPointValue( +_MEDIUM_HIGH = DecisionPointValue( name="Medium-High", key="MH", description="A successful exploit of this vulnerability may result in significant physical or property damage or loss.", ) -_LOW_MEDIUM = SsvcDecisionPointValue( +_LOW_MEDIUM = DecisionPointValue( name="Low-Medium", key="LM", description="A successful exploit of this vulnerability may result in moderate physical or property damage or loss.", ) -_CDP_NONE_2 = SsvcDecisionPointValue( +_CDP_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no potential for loss of life, physical assets, productivity or revenue.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="A successful exploit of this vulnerability may result in catastrophic physical or property damage and loss. The range of effect may be over a wide area.", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="A successful exploit of this vulnerability may result in significant physical or property damage or loss.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="A successful exploit of this vulnerability may result in light physical or property damage or loss. The system itself may be damaged or destroyed.", ) -_CDP_NONE = SsvcDecisionPointValue( +_CDP_NONE = DecisionPointValue( name="None", key="N", description="There is no potential for physical or property damage.", diff --git a/src/ssvc/decision_points/cvss/confidentiality_impact.py b/src/ssvc/decision_points/cvss/confidentiality_impact.py index 0a4eb55b..c59512be 100644 --- a/src/ssvc/decision_points/cvss/confidentiality_impact.py +++ b/src/ssvc/decision_points/cvss/confidentiality_impact.py @@ -21,11 +21,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="There is total loss of confidentiality, resulting in all resources within the impacted component " @@ -34,7 +34,7 @@ "steals the administrator's password, or private encryption keys of a web server.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, " @@ -43,13 +43,13 @@ "impacted component.", ) -_CI_NONE_2 = SsvcDecisionPointValue( +_CI_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no loss of confidentiality within the impacted component.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="A total compromise of critical system information. A complete loss of system protection resulting in " @@ -57,7 +57,7 @@ "system's data (memory, files, etc).", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="There is considerable informational disclosure. Access to critical system files is possible. There " @@ -65,7 +65,7 @@ "the scope of the loss is constrained.", ) -_CI_NONE = SsvcDecisionPointValue( +_CI_NONE = DecisionPointValue( name="None", key="N", description="No impact on confidentiality.", @@ -104,7 +104,7 @@ """ -_HIGH_1 = SsvcDecisionPointValue( +_HIGH_1 = DecisionPointValue( name="High", key="H", description="There is total loss of confidentiality, resulting in all resources within the impacted component " @@ -113,7 +113,7 @@ "steals the administrator's password, or private encryption keys of a web server.", ) -_LOW_1 = SsvcDecisionPointValue( +_LOW_1 = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, " @@ -122,7 +122,7 @@ "impacted component.", ) -_CI_NONE_3 = SsvcDecisionPointValue( +_CI_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no loss of confidentiality within the impacted component.", diff --git a/src/ssvc/decision_points/cvss/confidentiality_requirement.py b/src/ssvc/decision_points/cvss/confidentiality_requirement.py index 782f2bf0..9c9c02e2 100644 --- a/src/ssvc/decision_points/cvss/confidentiality_requirement.py +++ b/src/ssvc/decision_points/cvss/confidentiality_requirement.py @@ -22,7 +22,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -30,21 +30,21 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of confidentiality is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of confidentiality is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of confidentiality is likely to have only a limited adverse effect on the organization or " @@ -81,21 +81,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of confidentiality is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of confidentiality is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of confidentiality is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/equivalence_set_1.py b/src/ssvc/decision_points/cvss/equivalence_set_1.py index 73d2788e..41a7de9e 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_1.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_1.py @@ -21,23 +21,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: AV:P or not(AV:N or PR:N or UI:N)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: (AV:N or PR:N or UI:N) and not (AV:N and PR:N and UI:N) and not AV:P", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: AV:N and PR:N and UI:N", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_2.py b/src/ssvc/decision_points/cvss/equivalence_set_2.py index d547d5c6..0e94a396 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_2.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_2.py @@ -21,7 +21,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -29,12 +29,12 @@ # Levels Constraints Highest Severity Vector(s) # 0 AC:L and AT:N AC:L/AT:N # 1 not (AC:L and AT:N) AC:L/AT:P or AC:H/AT:N -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Low", key="L", description="1: not (AC:L and AT:N)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: AC:L and AT:N", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_3.py b/src/ssvc/decision_points/cvss/equivalence_set_3.py index 6ddb938e..4b1f8492 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_3.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_3.py @@ -21,7 +21,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -30,17 +30,17 @@ # 0 VC:H and VI:H VC:H/VI:H/VA:H # 1 not (VC:H and VI:H) and (VC:H or VI:H or VA:H) VC:L/VI:H/VA:H or VC:H/VI:L/VA:H # 2 not (VC:H or VI:H or VA:H) VC:L/VI:L/VA:L -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: not (VC:H or VI:H or VA:H)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: not (VC:H and VI:H) and (VC:H or VI:H or VA:H)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: VC:H and VI:H", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_4.py b/src/ssvc/decision_points/cvss/equivalence_set_4.py index 4ac5d601..f57ed0a1 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_4.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_4.py @@ -21,7 +21,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -29,17 +29,17 @@ # 0 MSI:S or MSA:S SC:H/SI:S/SA:S # 1 not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H) SC:H/SI:H/SA:H # 2 not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H) SC:L/SI:L/SA:L -TWO = SsvcDecisionPointValue( +TWO = DecisionPointValue( name="Low", key="L", description="2: not (MSI:S or MSA:S) and not (SC:H or SI:H or SA:H)", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="Medium", key="M", description="1: not (MSI:S or MSA:S) and (SC:H or SI:H or SA:H)", ) -ZERO = SsvcDecisionPointValue( +ZERO = DecisionPointValue( name="High", key="H", description="0: MSI:S or MSA:S", diff --git a/src/ssvc/decision_points/cvss/equivalence_set_5.py b/src/ssvc/decision_points/cvss/equivalence_set_5.py index bb519273..fff4ebc9 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_5.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_5.py @@ -21,7 +21,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs @@ -29,9 +29,21 @@ # 0 E:A E:A # 1 E:P E:P # 2 E:U E:U -TWO = SsvcDecisionPointValue(name="Low", key="L", description="2: E:U", ) -ONE = SsvcDecisionPointValue(name="Medium", key="M", description="1: E:P", ) -ZERO = SsvcDecisionPointValue(name="High", key="H", description="0: E:A", ) +TWO = DecisionPointValue( + name="Low", + key="L", + description="2: E:U", +) +ONE = DecisionPointValue( + name="Medium", + key="M", + description="1: E:P", +) +ZERO = DecisionPointValue( + name="High", + key="H", + description="0: E:A", +) EQ5 = CvssDecisionPoint( name="Equivalence Set 5", key="EQ5", @@ -41,16 +53,17 @@ TWO, ONE, ZERO, -), + ), ) VERSIONS = (EQ5,) LATEST = VERSIONS[-1] + def main(): print_versions_and_diffs(VERSIONS) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/ssvc/decision_points/cvss/equivalence_set_6.py b/src/ssvc/decision_points/cvss/equivalence_set_6.py index 4fbd59e3..eecb8d69 100644 --- a/src/ssvc/decision_points/cvss/equivalence_set_6.py +++ b/src/ssvc/decision_points/cvss/equivalence_set_6.py @@ -21,17 +21,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs # EQ6 → VC/VI/VA+CR/CI/CA with 2 levels specified in Table 29 # 0 (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H) VC:H/VI:H/VA:H/CR:H/IR:H/AR:H # 1 not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H) VC:H/VI:H/VA:H/CR:M/IR:M/AR:M or VC:H/VI:H/VA:L/CR:M/IR:M/AR:H or VC:H/VI:L/VA:H/CR:M/IR:H/AR:M or VC:H/VI:L/VA:L/CR:M/IR:H/AR:H or VC:L/VI:H/VA:H/CR:H/IR:M/AR:M or VC:L/VI:H/VA:L/CR:H/IR:M/AR:H or VC:L/VI:L/VA:H/CR:H/IR:H/AR:M or VC:L/VI:L/VA:L/CR:H/IR:H/AR:H -ONE = SsvcDecisionPointValue(name="Low", key="L", - description="1: not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H)", ) -ZERO = SsvcDecisionPointValue(name="High", key="H", - description="0: (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)", ) +ONE = DecisionPointValue( + name="Low", + key="L", + description="1: not (CR:H and VC:H) and not (IR:H and VI:H) and not (AR:H and VA:H)", +) +ZERO = DecisionPointValue( + name="High", + key="H", + description="0: (CR:H and VC:H) or (IR:H and VI:H) or (AR:H and VA:H)", +) EQ6 = CvssDecisionPoint( name="Equivalence Set 6", key="EQ6", @@ -46,10 +52,10 @@ VERSIONS = (EQ6,) LATEST = VERSIONS[-1] + def main(): print_versions_and_diffs(VERSIONS) -if __name__ == '__main__': +if __name__ == "__main__": main() - diff --git a/src/ssvc/decision_points/cvss/exploit_maturity.py b/src/ssvc/decision_points/cvss/exploit_maturity.py index a1a82ae3..66a6e758 100644 --- a/src/ssvc/decision_points/cvss/exploit_maturity.py +++ b/src/ssvc/decision_points/cvss/exploit_maturity.py @@ -22,7 +22,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -30,7 +30,7 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Functional autonomous code exists, or no exploit is required (manual trigger) and details are widely " @@ -40,14 +40,14 @@ "easy-to-use automated tools.", ) -_FUNCTIONAL_2 = SsvcDecisionPointValue( +_FUNCTIONAL_2 = DecisionPointValue( name="Functional", key="F", description="Functional exploit code is available. The code works in most situations where the vulnerability " "exists.", ) -_PROOF_OF_CONCEPT_2 = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT_2 = DecisionPointValue( name="Proof-of-Concept", key="POC", description="Proof-of-concept exploit code is available, or an attack demonstration is not practical for most " @@ -55,13 +55,13 @@ "modification by a skilled attacker.", ) -_UNPROVEN_2 = SsvcDecisionPointValue( +_UNPROVEN_2 = DecisionPointValue( name="Unproven", key="U", description="No exploit code is available, or an exploit is theoretical.", ) -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Either the vulnerability is exploitable by functional mobile autonomous code or no exploit is " @@ -70,14 +70,14 @@ "via a mobile autonomous agent (a worm or virus).", ) -_FUNCTIONAL = SsvcDecisionPointValue( +_FUNCTIONAL = DecisionPointValue( name="Functional", key="F", description="Functional exploit code is available. The code works in most situations where the vulnerability is " "exploitable.", ) -_PROOF_OF_CONCEPT = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT = DecisionPointValue( name="Proof of Concept", key="P", description="Proof of concept exploit code or an attack demonstration that is not practically applicable to " @@ -85,7 +85,7 @@ "require substantial hand tuning by a skilled attacker for use against deployed systems.", ) -_UNPROVEN = SsvcDecisionPointValue( +_UNPROVEN = DecisionPointValue( name="Unproven", key="U", description="No exploit code is yet available or an exploit method is entirely theoretical.", @@ -146,7 +146,7 @@ """ -_ATTACKED = SsvcDecisionPointValue( +_ATTACKED = DecisionPointValue( name="Attacked", key="A", description="Based on available threat intelligence either of the following must apply: Attacks targeting " @@ -154,7 +154,7 @@ "to exploit the vulnerability are publicly or privately available (such as exploit toolkits)", ) -_PROOF_OF_CONCEPT_3 = SsvcDecisionPointValue( +_PROOF_OF_CONCEPT_3 = DecisionPointValue( name="Proof-of-Concept", key="P", description="Based on available threat intelligence each of the following must apply: Proof-of-concept exploit " @@ -163,7 +163,7 @@ "(i.e., the “Attacked” value does not apply)", ) -_UNREPORTED = SsvcDecisionPointValue( +_UNREPORTED = DecisionPointValue( name="Unreported", key="U", description="Based on available threat intelligence each of the following must apply: No knowledge of publicly " diff --git a/src/ssvc/decision_points/cvss/helpers.py b/src/ssvc/decision_points/cvss/helpers.py index ebc45102..c2677735 100644 --- a/src/ssvc/decision_points/cvss/helpers.py +++ b/src/ssvc/decision_points/cvss/helpers.py @@ -23,29 +23,33 @@ from copy import deepcopy -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +import semver + +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X +from ssvc.decision_points.cvss.base import CvssDecisionPoint as DecisionPoint -def _modify_3(dp: SsvcDecisionPoint): - _dp = deepcopy(dp) - _dp.name = "Modified " + _dp.name - _dp.key = "M" + _dp.key +def _modify_3(dp: DecisionPoint): + _dp_dict = deepcopy(dp.model_dump()) + _dp_dict["name"] = f"Modified {_dp_dict["name"]}" + _dp_dict["key"] = f"M{_dp_dict["key"]}" # if there is no value named "Not Defined" value, add it nd = NOT_DEFINED_X - values = list(_dp.values) - - names = [v.name for v in values] + values = list(_dp_dict["values"]) + names = [v["name"] for v in values] if nd.name not in names: values.append(nd) - _dp.values = list(values) + _dp_dict["values"] = tuple(values) + + _dp = DecisionPoint(**_dp_dict) return _dp -def modify_3(dp: SsvcDecisionPoint): +def modify_3(dp: DecisionPoint): """ Prepends "Modified " to the name and "M" to the key of the given object. Also adds a value of "Not Defined" to the values list. @@ -57,11 +61,11 @@ def modify_3(dp: SsvcDecisionPoint): """ _dp = _modify_3(dp) - _dp.__post_init__() # call post-init to update the key & register + DecisionPoint.model_validate(_dp) # validate the modified object return _dp -def modify_4(dp: SsvcDecisionPoint): +def modify_4(dp: DecisionPoint): """ Modifies a CVSS v4 Base Metric decision point object. @@ -74,37 +78,41 @@ def modify_4(dp: SsvcDecisionPoint): _dp = _modify_3(dp) _dp = _modify_4(_dp) - _dp.__post_init__() # call post-init to update the key & register + DecisionPoint.model_validate(_dp) # validate the modified object return _dp -def _modify_4(dp: SsvcDecisionPoint): +def _modify_4(dp: DecisionPoint): # note: # this method was split out for testing purposes # assumes you've already done the 3.0 modifications - _dp = deepcopy(dp) - # Note: For MSC, MSI, and MSA, the lowest metric value is “Negligible” (N), not “None” (N). - if _dp.key in ["MSC", "MSI", "MSA"]: - for v in _dp.values: - if v.key == "N": - v.name = "Negligible" - v.description.replace(" no ", " negligible ") + _dp_dict = deepcopy(dp.model_dump()) + key = _dp_dict["key"] + if key in ["MSC", "MSI", "MSA"]: + for v in _dp_dict["values"]: + if v["key"] == "N": + v["name"] = "Negligible" + v["description"] = v["description"].replace(" no ", " negligible ") + # we need to bump the version for this change + _dp_dict["version"] = semver.bump_patch(_dp_dict["version"]) break # Note: For MSI, There is also a highest severity level, Safety (S), in addition to the same values as the # corresponding Base Metric (High, Medium, Low). - if _dp.key == "MSI": - _SAFETY = SsvcDecisionPointValue( + if key == "MSI": + _SAFETY = DecisionPointValue( name="Safety", key="S", description="The Safety metric value measures the impact regarding the Safety of a human actor or " "participant that can be predictably injured as a result of the vulnerability being exploited.", ) - values = list(_dp.values) + values = list(_dp_dict["values"]) values.append(_SAFETY) - _dp.values = list(values) + _dp_dict["values"] = tuple(values) + + _dp = DecisionPoint(**_dp_dict) return _dp diff --git a/src/ssvc/decision_points/cvss/impact_bias.py b/src/ssvc/decision_points/cvss/impact_bias.py index 8078b109..bf9b4a92 100644 --- a/src/ssvc/decision_points/cvss/impact_bias.py +++ b/src/ssvc/decision_points/cvss/impact_bias.py @@ -21,29 +21,29 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_AVAILABILITY = SsvcDecisionPointValue( +_AVAILABILITY = DecisionPointValue( name="Availability", key="A", description="Availability Impact is assigned greater weight than Confidentiality Impact or Integrity Impact.", ) -_INTEGRITY = SsvcDecisionPointValue( +_INTEGRITY = DecisionPointValue( name="Integrity", key="I", description="Integrity Impact is assigned greater weight than Confidentiality Impact or Availability Impact.", ) -_CONFIDENTIALITY = SsvcDecisionPointValue( +_CONFIDENTIALITY = DecisionPointValue( name="Confidentiality", key="C", description="Confidentiality impact is assigned greater weight than Integrity Impact or Availability Impact.", ) -_NORMAL = SsvcDecisionPointValue( +_NORMAL = DecisionPointValue( name="Normal", key="N", description="Confidentiality Impact, Integrity Impact, and Availability Impact are all assigned the same weight.", diff --git a/src/ssvc/decision_points/cvss/integrity_impact.py b/src/ssvc/decision_points/cvss/integrity_impact.py index 94e617a6..1d45a906 100644 --- a/src/ssvc/decision_points/cvss/integrity_impact.py +++ b/src/ssvc/decision_points/cvss/integrity_impact.py @@ -22,18 +22,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_II_HIGH = SsvcDecisionPointValue( +_II_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection.", ) -_II_LOW = SsvcDecisionPointValue( +_II_LOW = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -41,19 +41,19 @@ "direct, serious impact on the impacted component.", ) -_II_NONE_2 = SsvcDecisionPointValue( +_II_NONE_2 = DecisionPointValue( name="None", key="N", description="There is no impact to the integrity of the system.", ) -_COMPLETE = SsvcDecisionPointValue( +_COMPLETE = DecisionPointValue( name="Complete", key="C", description="A total compromise of system integrity. There is a complete loss of system protection resulting in " "the entire system being compromised. The attacker has sovereign control to modify any system files.", ) -_PARTIAL = SsvcDecisionPointValue( +_PARTIAL = DecisionPointValue( name="Partial", key="P", description="Considerable breach in integrity. Modification of critical system files or information is possible, " @@ -62,7 +62,7 @@ "but at random or in a limited context or scope.", ) -_II_NONE = SsvcDecisionPointValue( +_II_NONE = DecisionPointValue( name="None", key="N", description="No impact on integrity." ) @@ -97,13 +97,13 @@ Updates None. Removes Partial and Complete. Adds Low and High values for CVSS Integrity Impact. """ -_II_HIGH_2 = SsvcDecisionPointValue( +_II_HIGH_2 = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection.", ) -_II_LOW_2 = SsvcDecisionPointValue( +_II_LOW_2 = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -112,7 +112,7 @@ ) -_II_NONE_3 = SsvcDecisionPointValue( +_II_NONE_3 = DecisionPointValue( name="None", key="N", description="There is no loss of integrity within the Vulnerable System.", diff --git a/src/ssvc/decision_points/cvss/integrity_requirement.py b/src/ssvc/decision_points/cvss/integrity_requirement.py index af28beaf..2c8a652b 100644 --- a/src/ssvc/decision_points/cvss/integrity_requirement.py +++ b/src/ssvc/decision_points/cvss/integrity_requirement.py @@ -22,7 +22,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -30,21 +30,21 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Loss of integrity is likely to have a catastrophic adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Loss of integrity is likely to have a serious adverse effect on the organization or individuals " "associated with the organization (e.g., employees, customers).", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Loss of integrity is likely to have only a limited adverse effect on the organization or individuals " @@ -81,21 +81,21 @@ ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="Loss of integrity is likely to have a catastrophic adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_MEDIUM_2 = SsvcDecisionPointValue( +_MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="Loss of integrity is likely to have a serious adverse effect on the organization or " "individuals associated with the organization (e.g., employees, customers).", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="Loss of integrity is likely to have only a limited adverse effect on the organization or " diff --git a/src/ssvc/decision_points/cvss/privileges_required.py b/src/ssvc/decision_points/cvss/privileges_required.py index b0b5e922..92ab7667 100644 --- a/src/ssvc/decision_points/cvss/privileges_required.py +++ b/src/ssvc/decision_points/cvss/privileges_required.py @@ -21,11 +21,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="The attacker is authorized with (i.e. requires) privileges that provide significant (e.g. " @@ -33,7 +33,7 @@ "files.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="The attacker is authorized with (i.e. requires) privileges that provide basic user capabilities that " @@ -41,7 +41,7 @@ "privileges may have the ability to cause an impact only to non-sensitive resources.", ) -_PR_NONE = SsvcDecisionPointValue( +_PR_NONE = DecisionPointValue( name="None", key="N", description="The attacker is unauthorized prior to attack, and therefore does not require any access to settings " @@ -68,14 +68,14 @@ """ -_PR_NONE_2 = SsvcDecisionPointValue( +_PR_NONE_2 = DecisionPointValue( name="None", key="N", description="The attacker is unauthorized prior to attack, and therefore does not require any access to settings " "or files to carry out an attack.", ) -_LOW_2 = SsvcDecisionPointValue( +_LOW_2 = DecisionPointValue( name="Low", key="L", description="The attacker is authorized with (i.e., requires) privileges that provide basic capabilities that " @@ -83,7 +83,7 @@ "an attacker with Low privileges has the ability to access only non-sensitive resources.", ) -_HIGH_2 = SsvcDecisionPointValue( +_HIGH_2 = DecisionPointValue( name="High", key="H", description="The attacker is authorized with (i.e., requires) privileges that provide significant (e.g., " diff --git a/src/ssvc/decision_points/cvss/qualitative_severity.py b/src/ssvc/decision_points/cvss/qualitative_severity.py index 25173312..553fef6b 100644 --- a/src/ssvc/decision_points/cvss/qualitative_severity.py +++ b/src/ssvc/decision_points/cvss/qualitative_severity.py @@ -21,32 +21,32 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -QS_NONE = SsvcDecisionPointValue( +QS_NONE = DecisionPointValue( name="None", key="N", description="No severity rating (0.0)", ) -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="Low (0.1 - 3.9)", ) -MEDIUM = SsvcDecisionPointValue( +MEDIUM = DecisionPointValue( name="Medium", key="M", description="Medium (4.0 - 6.9)", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="High (7.0 - 8.9)", ) -CRITICAL = SsvcDecisionPointValue( +CRITICAL = DecisionPointValue( name="Critical", key="C", description="Critical (9.0 - 10.0)", diff --git a/src/ssvc/decision_points/cvss/remediation_level.py b/src/ssvc/decision_points/cvss/remediation_level.py index f3c9aad1..090d3eae 100644 --- a/src/ssvc/decision_points/cvss/remediation_level.py +++ b/src/ssvc/decision_points/cvss/remediation_level.py @@ -22,19 +22,19 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_UNAVAILABLE = SsvcDecisionPointValue( +_UNAVAILABLE = DecisionPointValue( name="Unavailable", key="U", description="There is either no solution available or it is impossible to apply.", ) -_WORKAROUND = SsvcDecisionPointValue( +_WORKAROUND = DecisionPointValue( name="Workaround", key="W", description="There is an unofficial, non-vendor solution available. In some cases, users of the affected " @@ -43,14 +43,14 @@ "plugging the hole for the mean time and no official remediation is available, this value can be set.", ) -_TEMPORARY_FIX = SsvcDecisionPointValue( +_TEMPORARY_FIX = DecisionPointValue( name="Temporary Fix", key="TF", description="There is an official but temporary fix available. This includes instances where the vendor issues a " "temporary hotfix, tool or official workaround.", ) -_OFFICIAL_FIX = SsvcDecisionPointValue( +_OFFICIAL_FIX = DecisionPointValue( name="Official Fix", key="OF", description="A complete vendor solution is available. Either the vendor has issued the final, official patch " diff --git a/src/ssvc/decision_points/cvss/report_confidence.py b/src/ssvc/decision_points/cvss/report_confidence.py index 15f9deda..5501363d 100644 --- a/src/ssvc/decision_points/cvss/report_confidence.py +++ b/src/ssvc/decision_points/cvss/report_confidence.py @@ -22,7 +22,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import ( NOT_DEFINED_ND, NOT_DEFINED_X, @@ -30,7 +30,7 @@ from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_CONFIRMED_2 = SsvcDecisionPointValue( +_CONFIRMED_2 = DecisionPointValue( name="Confirmed", key="C", description="Detailed reports exist, or functional reproduction is possible (functional exploits may provide " @@ -38,7 +38,7 @@ "or the author or vendor of the affected code has confirmed the presence of the vulnerability.", ) -_REASONABLE = SsvcDecisionPointValue( +_REASONABLE = DecisionPointValue( name="Reasonable", key="R", description="Significant details are published, but researchers either do not have full confidence in the root " @@ -47,7 +47,7 @@ "impact is able to be verified (proof-of-concept exploits may provide this).", ) -_UNKNOWN = SsvcDecisionPointValue( +_UNKNOWN = DecisionPointValue( name="Unknown", key="U", description="There are reports of impacts that indicate a vulnerability is present. The reports indicate that the " @@ -57,7 +57,7 @@ "differences described.", ) -_CONFIRMED = SsvcDecisionPointValue( +_CONFIRMED = DecisionPointValue( name="Confirmed", key="C", description="Vendor or author of the affected technology has acknowledged that the vulnerability exists. This " @@ -66,7 +66,7 @@ "widespread exploitation.", ) -_UNCORROBORATED = SsvcDecisionPointValue( +_UNCORROBORATED = DecisionPointValue( name="Uncorroborated", key="UR", description="Multiple non-official sources; possibily including independent security companies or research " @@ -74,7 +74,7 @@ "ambiguity.", ) -_UNCONFIRMED = SsvcDecisionPointValue( +_UNCONFIRMED = DecisionPointValue( name="Unconfirmed", key="UC", description="A single unconfirmed source or possibly several conflicting reports. There is little confidence in " diff --git a/src/ssvc/decision_points/cvss/scope.py b/src/ssvc/decision_points/cvss/scope.py index ab45522f..ecf89588 100644 --- a/src/ssvc/decision_points/cvss/scope.py +++ b/src/ssvc/decision_points/cvss/scope.py @@ -22,18 +22,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_CHANGED = SsvcDecisionPointValue( +_CHANGED = DecisionPointValue( name="Changed", key="C", description="An exploited vulnerability can affect resources beyond the authorization privileges intended by the " "vulnerable component. In this case the vulnerable component and the impacted component are different.", ) -_UNCHANGED = SsvcDecisionPointValue( +_UNCHANGED = DecisionPointValue( name="Unchanged", key="U", description="An exploited vulnerability can only affect resources managed by the same authority. In this case the " diff --git a/src/ssvc/decision_points/cvss/subsequent_availability_impact.py b/src/ssvc/decision_points/cvss/subsequent_availability_impact.py index 8cad819a..aa73c348 100644 --- a/src/ssvc/decision_points/cvss/subsequent_availability_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_availability_impact.py @@ -21,11 +21,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_SA_HIGH = SsvcDecisionPointValue( +_SA_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of availability, resulting in the attacker being able to fully deny access to " @@ -33,7 +33,7 @@ "deliver the attack) or persistent (the condition persists even after the attack has completed).", ) -_SA_LOW = SsvcDecisionPointValue( +_SA_LOW = DecisionPointValue( name="Low", key="L", description="Performance is reduced or there are interruptions in resource availability. Even if repeated " @@ -41,7 +41,7 @@ "deny service to legitimate users.", ) -_SA_NONE = SsvcDecisionPointValue( +_SA_NONE = DecisionPointValue( name="None", key="N", description="There is no impact to availability within the Subsequent System or all availability impact is " diff --git a/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py b/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py index 4705ac66..d61d91fc 100644 --- a/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_confidentiality_impact.py @@ -21,18 +21,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="There is no loss of confidentiality within the Subsequent System or all confidentiality impact is " "constrained to the Vulnerable System.", ) -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="There is some loss of confidentiality. Access to some restricted information is obtained, but the " @@ -40,7 +40,7 @@ "limited. The information disclosure does not cause a direct, serious loss to the Subsequent System.", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of confidentiality, resulting in all resources within the Subsequent System " diff --git a/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py b/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py index b6f37654..dbf53244 100644 --- a/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py +++ b/src/ssvc/decision_points/cvss/subsequent_integrity_impact.py @@ -21,11 +21,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -SI_HIGH = SsvcDecisionPointValue( +SI_HIGH = DecisionPointValue( name="High", key="H", description="There is a total loss of integrity, or a complete loss of protection. For example, the attacker is able " @@ -34,7 +34,7 @@ "System.", ) -SI_LOW = SsvcDecisionPointValue( +SI_LOW = DecisionPointValue( name="Low", key="L", description="Modification of data is possible, but the attacker does not have control over the consequence of a " @@ -42,7 +42,7 @@ "serious impact to the Subsequent System.", ) -SI_NONE = SsvcDecisionPointValue( +SI_NONE = DecisionPointValue( name="None", key="N", description="There is no loss of integrity within the Subsequent System or all integrity impact is constrained to " diff --git a/src/ssvc/decision_points/cvss/supplemental/automatable.py b/src/ssvc/decision_points/cvss/supplemental/automatable.py index 4591d2b2..5d430ca5 100644 --- a/src/ssvc/decision_points/cvss/supplemental/automatable.py +++ b/src/ssvc/decision_points/cvss/supplemental/automatable.py @@ -21,18 +21,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="Attackers cannot reliably automate all 4 steps of the kill chain for this vulnerability for " "some reason. These steps are reconnaissance, weaponization, delivery, and exploitation.", ) -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="Attackers can reliably automate all 4 steps of the kill chain. These steps are " diff --git a/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py b/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py index 0ea71e74..97968101 100644 --- a/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py +++ b/src/ssvc/decision_points/cvss/supplemental/provider_urgency.py @@ -21,27 +21,27 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -RED = SsvcDecisionPointValue( +RED = DecisionPointValue( name="Red", key="R", description="Provider has assessed the impact of this vulnerability as having the highest urgency.", ) -AMBER = SsvcDecisionPointValue( +AMBER = DecisionPointValue( name="Amber", key="A", description="Provider has assessed the impact of this vulnerability as having a moderate urgency.", ) -GREEN = SsvcDecisionPointValue( +GREEN = DecisionPointValue( name="Green", key="G", description="Provider has assessed the impact of this vulnerability as having a reduced urgency.", ) -CLEAR = SsvcDecisionPointValue( +CLEAR = DecisionPointValue( name="Clear", key="C", description="Provider has assessed the impact of this vulnerability as having no urgency (Informational).", diff --git a/src/ssvc/decision_points/cvss/supplemental/recovery.py b/src/ssvc/decision_points/cvss/supplemental/recovery.py index c2d16716..eaa58959 100644 --- a/src/ssvc/decision_points/cvss/supplemental/recovery.py +++ b/src/ssvc/decision_points/cvss/supplemental/recovery.py @@ -21,23 +21,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -AUTOMATIC = SsvcDecisionPointValue( +AUTOMATIC = DecisionPointValue( name="Automatic", key="A", description="The system recovers services automatically after an attack has been performed.", ) -USER = SsvcDecisionPointValue( +USER = DecisionPointValue( name="User", key="U", description="The system requires manual intervention by the user to recover services, after an attack has " "been performed.", ) -IRRECOVERABLE = SsvcDecisionPointValue( +IRRECOVERABLE = DecisionPointValue( name="Irrecoverable", key="I", description="The system services are irrecoverable by the user, after an attack has been performed.", diff --git a/src/ssvc/decision_points/cvss/supplemental/safety.py b/src/ssvc/decision_points/cvss/supplemental/safety.py index 3343bbf0..c05d62cf 100644 --- a/src/ssvc/decision_points/cvss/supplemental/safety.py +++ b/src/ssvc/decision_points/cvss/supplemental/safety.py @@ -22,18 +22,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -PRESENT = SsvcDecisionPointValue( +PRESENT = DecisionPointValue( name="Present", key="P", description="Consequences of the vulnerability meet definition of IEC 61508 consequence categories of " '"marginal," "critical," or "catastrophic."', ) -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="Consequences of the vulnerability meet definition of IEC 61508 consequence category " @@ -42,7 +42,7 @@ SAFETY_1 = CvssDecisionPoint( name="Safety", description="The Safety decision point is a measure of the potential for harm to humans or the environment.", - key="S", + key="SF", version="1.0.0", values=( NOT_DEFINED_X, diff --git a/src/ssvc/decision_points/cvss/supplemental/value_density.py b/src/ssvc/decision_points/cvss/supplemental/value_density.py index 0ee2151a..784b0bca 100644 --- a/src/ssvc/decision_points/cvss/supplemental/value_density.py +++ b/src/ssvc/decision_points/cvss/supplemental/value_density.py @@ -21,18 +21,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -DIFFUSE = SsvcDecisionPointValue( +DIFFUSE = DecisionPointValue( name="Diffuse", key="D", description="The vulnerable system has limited resources. That is, the resources that the attacker will " "gain control over with a single exploitation event are relatively small.", ) -CONCENTRATED = SsvcDecisionPointValue( +CONCENTRATED = DecisionPointValue( name="Concentrated", key="C", description="The vulnerable system is rich in resources. Heuristically, such systems are often the direct " diff --git a/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py b/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py index a1075413..b19f1799 100644 --- a/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py +++ b/src/ssvc/decision_points/cvss/supplemental/vulnerability_response_effort.py @@ -21,23 +21,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -LOW = SsvcDecisionPointValue( +LOW = DecisionPointValue( name="Low", key="L", description="The effort required to respond to a vulnerability is low/trivial.", ) -MODERATE = SsvcDecisionPointValue( +MODERATE = DecisionPointValue( name="Moderate", key="M", description="The actions required to respond to a vulnerability require some effort on behalf of the " "consumer and could cause minimal service impact to implement.", ) -HIGH = SsvcDecisionPointValue( +HIGH = DecisionPointValue( name="High", key="H", description="The actions required to respond to a vulnerability are significant and/or difficult, and may " diff --git a/src/ssvc/decision_points/cvss/target_distribution.py b/src/ssvc/decision_points/cvss/target_distribution.py index bf14250f..5cd78d88 100644 --- a/src/ssvc/decision_points/cvss/target_distribution.py +++ b/src/ssvc/decision_points/cvss/target_distribution.py @@ -22,34 +22,34 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss._not_defined import NOT_DEFINED_X from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_HIGH = SsvcDecisionPointValue( +_HIGH = DecisionPointValue( name="High", key="H", description="Targets exist inside the environment on a considerable scale. Between 50% - 100% of the total " "environment is considered at risk.", ) -_MEDIUM = SsvcDecisionPointValue( +_MEDIUM = DecisionPointValue( name="Medium", key="M", description="Targets exist inside the environment, but on a medium scale. Between 16% - 49% of the total " "environment is at risk.", ) -_LOW = SsvcDecisionPointValue( +_LOW = DecisionPointValue( name="Low", key="L", description="Targets exist inside the environment, but on a small scale. Between 1% - 15% of the total " "environment is at risk.", ) -_TD_NONE = SsvcDecisionPointValue( +_TD_NONE = DecisionPointValue( name="None", key="N", description="No target systems exist, or targets are so highly specialized that they only exist in a laboratory " diff --git a/src/ssvc/decision_points/cvss/user_interaction.py b/src/ssvc/decision_points/cvss/user_interaction.py index 752e52e7..a5e24477 100644 --- a/src/ssvc/decision_points/cvss/user_interaction.py +++ b/src/ssvc/decision_points/cvss/user_interaction.py @@ -22,18 +22,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint from ssvc.decision_points.helpers import print_versions_and_diffs -_REQUIRED = SsvcDecisionPointValue( +_REQUIRED = DecisionPointValue( name="Required", key="R", description="Successful exploitation of this vulnerability requires a user to take some action before the " "vulnerability can be exploited.", ) -_UI_NONE = SsvcDecisionPointValue( +_UI_NONE = DecisionPointValue( name="None", key="N", description="The vulnerable system can be exploited without interaction from any user.", @@ -55,14 +55,14 @@ Defines None and Required values for CVSS User Interaction. """ -_UI_NONE_2 = SsvcDecisionPointValue( +_UI_NONE_2 = DecisionPointValue( name="None", key="N", description="The vulnerable system can be exploited without interaction from any human user, other than the " "attacker.", ) -_PASSIVE = SsvcDecisionPointValue( +_PASSIVE = DecisionPointValue( name="Passive", key="P", description="Successful exploitation of this vulnerability requires limited interaction by the targeted user with " @@ -70,7 +70,7 @@ "and do not require that the user actively subvert protections built into the vulnerable system.", ) -_ACTIVE = SsvcDecisionPointValue( +_ACTIVE = DecisionPointValue( name="Active", key="A", description="Successful exploitation of this vulnerability requires a targeted user to perform specific, " diff --git a/src/ssvc/decision_points/helpers.py b/src/ssvc/decision_points/helpers.py index 60b8aff5..9da189ae 100644 --- a/src/ssvc/decision_points/helpers.py +++ b/src/ssvc/decision_points/helpers.py @@ -26,10 +26,10 @@ import semver -from ssvc.decision_points import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPoint -def dp_diff(dp1: SsvcDecisionPoint, dp2: SsvcDecisionPoint) -> list[str]: +def dp_diff(dp1: DecisionPoint, dp2: DecisionPoint) -> list[str]: """ Compares two decision points and returns a list of differences. @@ -218,7 +218,7 @@ def dp_diff(dp1: SsvcDecisionPoint, dp2: SsvcDecisionPoint) -> list[str]: return diffs -def show_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: +def show_diffs(versions: Sequence[DecisionPoint]) -> None: if len(versions) < 2: print("Not enough versions to compare") return @@ -229,7 +229,7 @@ def show_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: print() -def print_versions_and_diffs(versions: Sequence[SsvcDecisionPoint]) -> None: +def print_versions_and_diffs(versions: Sequence[DecisionPoint]) -> None: """ Prints the json representation of each version and then shows the diffs between each version. diff --git a/src/ssvc/decision_points/ssvc/__init__.py b/src/ssvc/decision_points/ssvc/__init__.py new file mode 100644 index 00000000..03abf5f2 --- /dev/null +++ b/src/ssvc/decision_points/ssvc/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +""" +This package contains SSVC decision points belonging to the `ssvc` namespace. +""" diff --git a/src/ssvc/decision_points/automatable.py b/src/ssvc/decision_points/ssvc/automatable.py similarity index 91% rename from src/ssvc/decision_points/automatable.py rename to src/ssvc/decision_points/ssvc/automatable.py index cc640f54..3ebe61ff 100644 --- a/src/ssvc/decision_points/automatable.py +++ b/src/ssvc/decision_points/ssvc/automatable.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -RAPID = SsvcDecisionPointValue( +RAPID = DecisionPointValue( name="Rapid", key="R", description="Steps 1-4 of the of the kill chain can be reliably automated. If the vulnerability allows remote " "code execution or command injection, the default response should be rapid.", ) -SLOW = SsvcDecisionPointValue( +SLOW = DecisionPointValue( name="Slow", key="S", description="Steps 1-4 of the kill chain cannot be reliably automated for this vulnerability for some reason. " @@ -50,13 +51,13 @@ ) -AUT_NO = SsvcDecisionPointValue( +AUT_NO = DecisionPointValue( name="No", key="N", description="Attackers cannot reliably automate steps 1-4 of the kill chain for this vulnerability. " "These steps are (1) reconnaissance, (2) weaponization, (3) delivery, and (4) exploitation.", ) -AUT_YES = SsvcDecisionPointValue( +AUT_YES = DecisionPointValue( name="Yes", key="Y", description="Attackers can reliably automate steps 1-4 of the kill chain.", diff --git a/src/outcomes_to_json.py b/src/ssvc/decision_points/ssvc/base.py similarity index 72% rename from src/outcomes_to_json.py rename to src/ssvc/decision_points/ssvc/base.py index 4ae6f0e1..9fe2640c 100644 --- a/src/outcomes_to_json.py +++ b/src/ssvc/decision_points/ssvc/base.py @@ -1,4 +1,6 @@ -#!/usr/bin/python3 +""" +Provides the base class for all decision points in the `ssvc` namespace. +""" # Copyright (c) 2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE @@ -19,17 +21,15 @@ # subject to its own license. # DM24-0278 -from ssvc.outcomes import groups -from ssvc.outcomes.base import OutcomeGroup +from pydantic import BaseModel +from ssvc.decision_points.base import DecisionPoint +from ssvc.namespaces import NameSpace -def main(): - for x in dir(groups): - outcome = getattr(groups, x) - if type(outcome) == OutcomeGroup: - with open(f"../data/json/outcomes/{x}.json", "w") as f: - f.write(outcome.model_dump_json(indent=2)) +class SsvcDecisionPoint(DecisionPoint, BaseModel): + """ + Models a single decision point as a list of values. + """ -if __name__ == "__main__": - main() + namespace: str = NameSpace.SSVC diff --git a/src/ssvc/decision_points/critical_software.py b/src/ssvc/decision_points/ssvc/critical_software.py similarity index 91% rename from src/ssvc/decision_points/critical_software.py rename to src/ssvc/decision_points/ssvc/critical_software.py index d89ee309..6fad730c 100644 --- a/src/ssvc/decision_points/critical_software.py +++ b/src/ssvc/decision_points/ssvc/critical_software.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="System meets a critical software definition.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="System does not meet a critical software definition.", diff --git a/src/ssvc/decision_points/exploitation.py b/src/ssvc/decision_points/ssvc/exploitation.py similarity index 92% rename from src/ssvc/decision_points/exploitation.py rename to src/ssvc/decision_points/ssvc/exploitation.py index ff66718d..c4a4ccc8 100644 --- a/src/ssvc/decision_points/exploitation.py +++ b/src/ssvc/decision_points/ssvc/exploitation.py @@ -21,17 +21,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -ACTIVE = SsvcDecisionPointValue( +ACTIVE = DecisionPointValue( name="Active", key="A", description="Shared, observable, reliable evidence that the exploit is being" " used in the wild by real attackers; there is credible public reporting.", ) -POC_1 = SsvcDecisionPointValue( +POC_1 = DecisionPointValue( name="PoC", key="P", description="One of the following cases is true: (1) private evidence of exploitation is attested but not shared; " @@ -39,13 +40,13 @@ " or ExploitDB; or (4) the vulnerability has a well-known method of exploitation.", ) -POC_2 = SsvcDecisionPointValue( +POC_2 = DecisionPointValue( name="Public PoC", key="P", description="One of the following is true: (1) Typical public PoC exists in sources such as Metasploit or websites like ExploitDB; or (2) the vulnerability has a well-known method of exploitation.", ) -EXP_NONE = SsvcDecisionPointValue( +EXP_NONE = DecisionPointValue( name="None", key="N", description="There is no evidence of active exploitation and no public proof of concept (PoC) of how to exploit the vulnerability.", diff --git a/src/ssvc/decision_points/high_value_asset.py b/src/ssvc/decision_points/ssvc/high_value_asset.py similarity index 91% rename from src/ssvc/decision_points/high_value_asset.py rename to src/ssvc/decision_points/ssvc/high_value_asset.py index 89b80ff3..3612b094 100644 --- a/src/ssvc/decision_points/high_value_asset.py +++ b/src/ssvc/decision_points/ssvc/high_value_asset.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="System meets a high value asset definition.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="System does not meet a high value asset definition.", diff --git a/src/ssvc/decision_points/human_impact.py b/src/ssvc/decision_points/ssvc/human_impact.py similarity index 89% rename from src/ssvc/decision_points/human_impact.py rename to src/ssvc/decision_points/ssvc/human_impact.py index 8ffdf53a..a5a583bf 100644 --- a/src/ssvc/decision_points/human_impact.py +++ b/src/ssvc/decision_points/ssvc/human_impact.py @@ -22,65 +22,66 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -LOW_1 = SsvcDecisionPointValue( +LOW_1 = DecisionPointValue( name="Low", key="L", description="Mission Prevalence:Minimal AND Public Well-Being Impact:Minimal", ) -LOW_2 = SsvcDecisionPointValue( +LOW_2 = DecisionPointValue( name="Low", key="L", description="Safety Impact:(None OR Minor) AND Mission Impact:(None OR Degraded OR Crippled)", ) -LOW_3 = SsvcDecisionPointValue( +LOW_3 = DecisionPointValue( name="Low", key="L", description="Safety Impact:(Negligible) AND Mission Impact:(None OR Degraded OR Crippled)", ) -MEDIUM_1 = SsvcDecisionPointValue( +MEDIUM_1 = DecisionPointValue( name="Medium", key="M", description="Mission Prevalence:Support AND Public Well-Being Impact:(Minimal OR Material)", ) -MEDIUM_2 = SsvcDecisionPointValue( +MEDIUM_2 = DecisionPointValue( name="Medium", key="M", description="(Safety Impact:(None OR Minor) AND Mission Impact:MEF Failure) OR (Safety Impact:Major AND Mission Impact:(None OR Degraded OR Crippled))", ) -MEDIUM_3 = SsvcDecisionPointValue( +MEDIUM_3 = DecisionPointValue( name="Medium", key="M", description="(Safety Impact:Negligible AND Mission Impact:MEF Failure) OR (Safety Impact:Marginal AND Mission Impact:(None OR Degraded OR Crippled))", ) -HIGH_1 = SsvcDecisionPointValue( +HIGH_1 = DecisionPointValue( name="High", key="H", description="Mission Prevalence:Essential OR Public Well-Being Impact:(Irreversible)", ) -HIGH_2 = SsvcDecisionPointValue( +HIGH_2 = DecisionPointValue( name="High", key="H", description="(Safety Impact:Hazardous AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Major AND Mission Impact:MEF Failure)", ) -HIGH_3 = SsvcDecisionPointValue( +HIGH_3 = DecisionPointValue( name="High", key="H", description="(Safety Impact:Critical AND Mission Impact:(None OR Degraded OR Crippled)) OR (Safety Impact:Marginal AND Mission Impact:MEF Failure)", ) -VERY_HIGH_1 = SsvcDecisionPointValue( +VERY_HIGH_1 = DecisionPointValue( name="Very High", key="VH", description="Safety Impact:Catastrophic OR Mission Impact:Mission Failure", diff --git a/src/ssvc/decision_points/in_kev.py b/src/ssvc/decision_points/ssvc/in_kev.py similarity index 91% rename from src/ssvc/decision_points/in_kev.py rename to src/ssvc/decision_points/ssvc/in_kev.py index 455432aa..0e8c83bd 100644 --- a/src/ssvc/decision_points/in_kev.py +++ b/src/ssvc/decision_points/ssvc/in_kev.py @@ -21,16 +21,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="Vulnerability is listed in KEV.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="Vulnerability is not listed in KEV.", diff --git a/src/ssvc/decision_points/mission_impact.py b/src/ssvc/decision_points/ssvc/mission_impact.py similarity index 88% rename from src/ssvc/decision_points/mission_impact.py rename to src/ssvc/decision_points/ssvc/mission_impact.py index 8bd3df2b..c9666fb2 100644 --- a/src/ssvc/decision_points/mission_impact.py +++ b/src/ssvc/decision_points/ssvc/mission_impact.py @@ -23,40 +23,39 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -MISSION_FAILURE = SsvcDecisionPointValue( +MISSION_FAILURE = DecisionPointValue( name="Mission Failure", key="MF", description="Multiple or all mission essential functions fail; ability to recover those functions degraded; organization’s ability to deliver its overall mission fails", ) -MEF_FAILURE = SsvcDecisionPointValue( +MEF_FAILURE = DecisionPointValue( name="MEF Failure", key="MEF", description="Any one mission essential function fails for period of time longer than acceptable; overall mission of the organization degraded but can still be accomplished for a time", ) -MEF_CRIPPLED = SsvcDecisionPointValue( +MEF_CRIPPLED = DecisionPointValue( name="MEF Support Crippled", key="MSC", description="Activities that directly support essential functions are crippled; essential functions continue for a time", ) -MI_NED = SsvcDecisionPointValue( +MI_NED = DecisionPointValue( name="Non-Essential Degraded", key="NED", description="Degradation of non-essential functions; chronic degradation would eventually harm essential functions", ) -MI_NONE = SsvcDecisionPointValue( - name="None", key="N", description="Little to no impact" -) +MI_NONE = DecisionPointValue(name="None", key="N", description="Little to no impact") # combine MI_NONE and MI_NED into a single value -DEGRADED = SsvcDecisionPointValue( +DEGRADED = DecisionPointValue( name="Degraded", key="D", description="Little to no impact up to degradation of non-essential functions; chronic degradation would eventually harm essential functions", diff --git a/src/ssvc/decision_points/public_safety_impact.py b/src/ssvc/decision_points/ssvc/public_safety_impact.py similarity index 92% rename from src/ssvc/decision_points/public_safety_impact.py rename to src/ssvc/decision_points/ssvc/public_safety_impact.py index d49257a9..449c8938 100644 --- a/src/ssvc/decision_points/public_safety_impact.py +++ b/src/ssvc/decision_points/ssvc/public_safety_impact.py @@ -23,16 +23,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -MINIMAL_1 = SsvcDecisionPointValue( +MINIMAL_1 = DecisionPointValue( name="Minimal", description="The effect is below the threshold for all aspects described in material. ", key="M", ) -MATERIAL = SsvcDecisionPointValue( +MATERIAL = DecisionPointValue( name="Material", description="Any one or more of these conditions hold. " "Physical harm: Does one or more of the following: " @@ -47,7 +48,7 @@ key="M", ) -IRREVERSIBLE = SsvcDecisionPointValue( +IRREVERSIBLE = DecisionPointValue( name="Irreversible", description="Any one or more of these conditions hold. " "Physical harm: One or both of the following are true: (a) Multiple fatalities are likely." @@ -60,23 +61,23 @@ key="I", ) -SIGNIFICANT = SsvcDecisionPointValue( +SIGNIFICANT = DecisionPointValue( name="Significant", description="Safety Impact:(Major OR Hazardous OR Catastrophic)", key="S", ) -MINIMAL_2 = SsvcDecisionPointValue( +MINIMAL_2 = DecisionPointValue( name="Minimal", description="Safety Impact:(None OR Minor)", key="M" ) -SIGNIFICANT_1 = SsvcDecisionPointValue( +SIGNIFICANT_1 = DecisionPointValue( name="Significant", description="Safety Impact:(Marginal OR Critical OR Catastrophic)", key="S", ) -MINIMAL_3 = SsvcDecisionPointValue( +MINIMAL_3 = DecisionPointValue( name="Minimal", description="Safety Impact:Negligible", key="M" ) diff --git a/src/ssvc/decision_points/public_value_added.py b/src/ssvc/decision_points/ssvc/public_value_added.py similarity index 91% rename from src/ssvc/decision_points/public_value_added.py rename to src/ssvc/decision_points/ssvc/public_value_added.py index cee88ddd..eac5543b 100644 --- a/src/ssvc/decision_points/public_value_added.py +++ b/src/ssvc/decision_points/ssvc/public_value_added.py @@ -23,22 +23,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -LIMITED = SsvcDecisionPointValue( +LIMITED = DecisionPointValue( name="Limited", key="L", description="Minimal value added to the existing public information because existing information is already high quality and in multiple outlets.", ) -AMPLIATIVE = SsvcDecisionPointValue( +AMPLIATIVE = DecisionPointValue( name="Ampliative", key="A", description="Amplifies and/or augments the existing public information about the vulnerability, for example, adds additional detail, addresses or corrects errors in other public information, draws further attention to the vulnerability, etc.", ) -PRECEDENCE = SsvcDecisionPointValue( +PRECEDENCE = DecisionPointValue( name="Precedence", key="P", description="The publication would be the first publicly available, or be coincident with the first publicly available.", diff --git a/src/ssvc/decision_points/report_credibility.py b/src/ssvc/decision_points/ssvc/report_credibility.py similarity index 90% rename from src/ssvc/decision_points/report_credibility.py rename to src/ssvc/decision_points/ssvc/report_credibility.py index d7b3213b..e74218ce 100644 --- a/src/ssvc/decision_points/report_credibility.py +++ b/src/ssvc/decision_points/ssvc/report_credibility.py @@ -23,16 +23,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -NOT_CREDIBLE = SsvcDecisionPointValue( +NOT_CREDIBLE = DecisionPointValue( name="Not Credible", key="NC", description="The report is not credible.", ) -CREDIBLE = SsvcDecisionPointValue( +CREDIBLE = DecisionPointValue( name="Credible", key="C", description="The report is credible.", diff --git a/src/ssvc/decision_points/report_public.py b/src/ssvc/decision_points/ssvc/report_public.py similarity index 91% rename from src/ssvc/decision_points/report_public.py rename to src/ssvc/decision_points/ssvc/report_public.py index 73d34176..73cccfee 100644 --- a/src/ssvc/decision_points/report_public.py +++ b/src/ssvc/decision_points/ssvc/report_public.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="A public report of the vulnerability exists.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="No public report of the vulnerability exists.", diff --git a/src/ssvc/decision_points/safety_impact.py b/src/ssvc/decision_points/ssvc/safety_impact.py similarity index 95% rename from src/ssvc/decision_points/safety_impact.py rename to src/ssvc/decision_points/ssvc/safety_impact.py index 0d1f26f5..5db63999 100644 --- a/src/ssvc/decision_points/safety_impact.py +++ b/src/ssvc/decision_points/ssvc/safety_impact.py @@ -23,10 +23,11 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -CATASTROPHIC = SsvcDecisionPointValue( +CATASTROPHIC = DecisionPointValue( name="Catastrophic", key="C", description="Any one or more of these conditions hold. " @@ -38,7 +39,7 @@ "Psychological: N/A.", ) -HAZARDOUS = SsvcDecisionPointValue( +HAZARDOUS = DecisionPointValue( name="Hazardous", key="H", description="Any one or more of these conditions hold. " @@ -50,7 +51,7 @@ "Psychological: N/A.", ) -MAJOR = SsvcDecisionPointValue( +MAJOR = DecisionPointValue( name="Major", key="J", description="Any one or more of these conditions hold. " @@ -63,7 +64,7 @@ "Psychological: Widespread emotional or psychological harm, sufficient to be cause for counselling or therapy, to populations of people.", ) -MINOR = SsvcDecisionPointValue( +MINOR = DecisionPointValue( name="Minor", key="M", description="Any one or more of these conditions hold. " @@ -76,7 +77,7 @@ "Psychological: Emotional or psychological harm, sufficient to be cause for counselling or therapy, to multiple persons.", ) -SAF_NONE = SsvcDecisionPointValue( +SAF_NONE = DecisionPointValue( name="None", key="N", description="The effect is below the threshold for all aspects described in Minor.", @@ -85,7 +86,7 @@ ## Based on the IEC 61508 standard ## Catastrophic, Critical, Marginal, Negligible -CATASTROPHIC_2 = SsvcDecisionPointValue( +CATASTROPHIC_2 = DecisionPointValue( name="Catastrophic", key="C", description="Any one or more of these conditions hold.

" @@ -97,7 +98,7 @@ "- *Psychological*: N/A.", ) -CRITICAL = SsvcDecisionPointValue( +CRITICAL = DecisionPointValue( name="Critical", key="R", description="Any one or more of these conditions hold.

" @@ -109,7 +110,7 @@ "- *Psychological*: N/A.", ) -MARGINAL = SsvcDecisionPointValue( +MARGINAL = DecisionPointValue( name="Marginal", key="M", description="Any one or more of these conditions hold.

" @@ -122,7 +123,7 @@ "- *Psychological*: Widespread emotional or psychological harm, sufficient to be cause for counselling or therapy, to populations of people.", ) -NEGLIGIBLE = SsvcDecisionPointValue( +NEGLIGIBLE = DecisionPointValue( name="Negligible", key="N", description="Any one or more of these conditions hold.

" diff --git a/src/ssvc/decision_points/supplier_cardinality.py b/src/ssvc/decision_points/ssvc/supplier_cardinality.py similarity index 91% rename from src/ssvc/decision_points/supplier_cardinality.py rename to src/ssvc/decision_points/ssvc/supplier_cardinality.py index 0e27ff65..c78167f6 100644 --- a/src/ssvc/decision_points/supplier_cardinality.py +++ b/src/ssvc/decision_points/ssvc/supplier_cardinality.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -MULTIPLE = SsvcDecisionPointValue( +MULTIPLE = DecisionPointValue( name="Multiple", key="M", description="There are multiple suppliers of the vulnerable component.", ) -ONE = SsvcDecisionPointValue( +ONE = DecisionPointValue( name="One", key="O", description="There is only one supplier of the vulnerable component.", diff --git a/src/ssvc/decision_points/supplier_contacted.py b/src/ssvc/decision_points/ssvc/supplier_contacted.py similarity index 90% rename from src/ssvc/decision_points/supplier_contacted.py rename to src/ssvc/decision_points/ssvc/supplier_contacted.py index c74af9e1..2b6d16d9 100644 --- a/src/ssvc/decision_points/supplier_contacted.py +++ b/src/ssvc/decision_points/ssvc/supplier_contacted.py @@ -21,16 +21,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -YES = SsvcDecisionPointValue( +YES = DecisionPointValue( name="Yes", key="Y", description="The supplier has been contacted.", ) -NO = SsvcDecisionPointValue( +NO = DecisionPointValue( name="No", key="N", description="The supplier has not been contacted.", @@ -39,7 +40,7 @@ SUPPLIER_CONTACTED_1 = SsvcDecisionPoint( name="Supplier Contacted", description="Has the reporter made a good-faith effort to contact the supplier of the vulnerable component using a quality contact method?", - key="SC", + key="SCON", version="1.0.0", values=( NO, diff --git a/src/ssvc/decision_points/supplier_engagement.py b/src/ssvc/decision_points/ssvc/supplier_engagement.py similarity index 91% rename from src/ssvc/decision_points/supplier_engagement.py rename to src/ssvc/decision_points/ssvc/supplier_engagement.py index 827ecd68..ed9660fb 100644 --- a/src/ssvc/decision_points/supplier_engagement.py +++ b/src/ssvc/decision_points/ssvc/supplier_engagement.py @@ -23,16 +23,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -UNRESPONSIVE = SsvcDecisionPointValue( +UNRESPONSIVE = DecisionPointValue( name="Unresponsive", key="U", description="The supplier is not responding to the reporter’s contact effort and not actively participating in the coordination effort.", ) -ACTIVE = SsvcDecisionPointValue( +ACTIVE = DecisionPointValue( name="Active", key="A", description="The supplier is responding to the reporter’s contact effort and actively participating in the coordination effort.", diff --git a/src/ssvc/decision_points/supplier_involvement.py b/src/ssvc/decision_points/ssvc/supplier_involvement.py similarity index 89% rename from src/ssvc/decision_points/supplier_involvement.py rename to src/ssvc/decision_points/ssvc/supplier_involvement.py index 2bae21b7..253620d9 100644 --- a/src/ssvc/decision_points/supplier_involvement.py +++ b/src/ssvc/decision_points/ssvc/supplier_involvement.py @@ -22,22 +22,23 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -UNCOOPERATIVE = SsvcDecisionPointValue( +UNCOOPERATIVE = DecisionPointValue( name="Uncooperative/Unresponsive", key="UU", description="The supplier has not responded, declined to generate a remediation, or no longer exists.", ) -COOPERATIVE = SsvcDecisionPointValue( +COOPERATIVE = DecisionPointValue( name="Cooperative", key="C", description="The supplier is actively generating a patch or fix; they may or may not have provided a mitigation or work-around in the mean time.", ) -FIX_READY = SsvcDecisionPointValue( +FIX_READY = DecisionPointValue( name="Fix Ready", key="FR", description="The supplier has provided a patch or fix.", @@ -46,7 +47,7 @@ SUPPLIER_INVOLVEMENT_1 = SsvcDecisionPoint( name="Supplier Involvement", description="What is the state of the supplier’s work on addressing the vulnerability?", - key="SI", + key="SINV", version="1.0.0", values=( FIX_READY, diff --git a/src/ssvc/decision_points/system_exposure.py b/src/ssvc/decision_points/ssvc/system_exposure.py similarity index 92% rename from src/ssvc/decision_points/system_exposure.py rename to src/ssvc/decision_points/ssvc/system_exposure.py index a330f1ae..06b87b82 100644 --- a/src/ssvc/decision_points/system_exposure.py +++ b/src/ssvc/decision_points/ssvc/system_exposure.py @@ -22,17 +22,18 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -EXP_UNAVOIDABLE = SsvcDecisionPointValue( +EXP_UNAVOIDABLE = DecisionPointValue( name="Unavoidable", key="U", description="Internet or another widely accessible network where access cannot plausibly be restricted or " "controlled (e.g., DNS servers, web servers, VOIP servers, email servers)", ) -EXP_CONTROLLED = SsvcDecisionPointValue( +EXP_CONTROLLED = DecisionPointValue( name="Controlled", key="C", description="Networked service with some access restrictions or mitigations already in place (whether locally or on the network). " @@ -43,7 +44,7 @@ "execute it, then exposure should be small.", ) -EXP_SMALL = SsvcDecisionPointValue( +EXP_SMALL = DecisionPointValue( name="Small", key="S", description="Local service or program; highly controlled network", @@ -63,7 +64,7 @@ ) # EXP_OPEN is just a rename of EXP_UNAVOIDABLE -EXP_OPEN = SsvcDecisionPointValue( +EXP_OPEN = DecisionPointValue( name="Open", key="O", description="Internet or another widely accessible network where access cannot plausibly be restricted or " diff --git a/src/ssvc/decision_points/technical_impact.py b/src/ssvc/decision_points/ssvc/technical_impact.py similarity index 92% rename from src/ssvc/decision_points/technical_impact.py rename to src/ssvc/decision_points/ssvc/technical_impact.py index 1c537d66..e2bd46dd 100644 --- a/src/ssvc/decision_points/technical_impact.py +++ b/src/ssvc/decision_points/ssvc/technical_impact.py @@ -23,16 +23,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -TOTAL = SsvcDecisionPointValue( +TOTAL = DecisionPointValue( name="Total", key="T", description="The exploit gives the adversary total control over the behavior of the software, or it gives total disclosure of all information on the system that contains the vulnerability.", ) -PARTIAL = SsvcDecisionPointValue( +PARTIAL = DecisionPointValue( name="Partial", key="P", description="The exploit gives the adversary limited control over, or information exposure about, the behavior of the software that contains the vulnerability. Or the exploit gives the adversary an importantly low stochastic opportunity for total control.", diff --git a/src/ssvc/decision_points/utility.py b/src/ssvc/decision_points/ssvc/utility.py similarity index 88% rename from src/ssvc/decision_points/utility.py rename to src/ssvc/decision_points/ssvc/utility.py index f721fa4b..f83518c3 100644 --- a/src/ssvc/decision_points/utility.py +++ b/src/ssvc/decision_points/ssvc/utility.py @@ -23,40 +23,41 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -SUPER_EFFECTIVE_2 = SsvcDecisionPointValue( +SUPER_EFFECTIVE_2 = DecisionPointValue( name="Super Effective", key="S", description="Automatable:Yes AND Value Density:Concentrated", ) -EFFICIENT_2 = SsvcDecisionPointValue( +EFFICIENT_2 = DecisionPointValue( name="Efficient", key="E", description="(Automatable:Yes AND Value Density:Diffuse) OR (Automatable:No AND Value Density:Concentrated)", ) -LABORIOUS_2 = SsvcDecisionPointValue( +LABORIOUS_2 = DecisionPointValue( name="Laborious", key="L", description="Automatable:No AND Value Density:Diffuse", ) -SUPER_EFFECTIVE = SsvcDecisionPointValue( +SUPER_EFFECTIVE = DecisionPointValue( name="Super Effective", key="S", description="Virulence:Rapid and Value Density:Concentrated", ) -EFFICIENT = SsvcDecisionPointValue( +EFFICIENT = DecisionPointValue( name="Efficient", key="E", description="Virulence:Rapid and Value Density:Diffuse OR Virulence:Slow and Value Density:Concentrated", ) -LABORIOUS = SsvcDecisionPointValue( +LABORIOUS = DecisionPointValue( name="Laborious", key="L", description="Virulence:Slow and Value Density:Diffuse", diff --git a/src/ssvc/decision_points/value_density.py b/src/ssvc/decision_points/ssvc/value_density.py similarity index 91% rename from src/ssvc/decision_points/value_density.py rename to src/ssvc/decision_points/ssvc/value_density.py index d4a887a5..610291a8 100644 --- a/src/ssvc/decision_points/value_density.py +++ b/src/ssvc/decision_points/ssvc/value_density.py @@ -22,16 +22,17 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPointValue from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint -CONCENTRATED = SsvcDecisionPointValue( +CONCENTRATED = DecisionPointValue( name="Concentrated", key="C", description="The system that contains the vulnerable component is rich in resources. Heuristically, such systems are often the direct responsibility of “system operators” rather than users.", ) -DIFFUSE = SsvcDecisionPointValue( +DIFFUSE = DecisionPointValue( name="Diffuse", key="D", description="The system that contains the vulnerable component has limited resources. That is, the resources that the adversary will gain control over with a single exploitation event are relatively small.", diff --git a/src/ssvc/doc_helpers.py b/src/ssvc/doc_helpers.py index 5a3a30c7..9ab8a8dc 100644 --- a/src/ssvc/doc_helpers.py +++ b/src/ssvc/doc_helpers.py @@ -23,7 +23,7 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint MD_TABLE_ROW_TEMPLATE = "| {value.name} | {value.description} |" diff --git a/src/ssvc/doctools.py b/src/ssvc/doctools.py old mode 100644 new mode 100755 index 0655281d..b345ff26 --- a/src/ssvc/doctools.py +++ b/src/ssvc/doctools.py @@ -34,19 +34,49 @@ python -m ssvc.doctools --overwrite --jsondir data/json/decision_points """ +import importlib import logging import os +import re -import ssvc.dp_groups.cvss.collections # noqa -import ssvc.dp_groups.ssvc.collections # noqa from ssvc.decision_points.base import ( + DecisionPoint, REGISTERED_DECISION_POINTS, - SsvcDecisionPoint, ) +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint logger = logging.getLogger(__name__) +def find_modules_to_import( + directory: str = "../decision_points", package: str = "ssvc.decision_points" +) -> bool: + """ + Find all modules that contain decision points and import them. + + This is necessary to ensure that all decision points are registered. + """ + imported_modules = [] + for root, _, files in os.walk(os.path.abspath(directory)): + for file in files: + if file.endswith(".py") and not file.startswith("__"): + # build the module name relative to the package + relative_path = os.path.relpath(root, directory) + module_name = os.path.join(relative_path, file[:-3]).replace( + os.sep, "." + ) + + full_module_name = f"{package}.{module_name}" + # import the module + try: + logger.info(f"Importing module {full_module_name}") + module = importlib.import_module(full_module_name) + imported_modules.append(module) + except ImportError as e: + logger.error(f"Failed to import {full_module_name}: {e}") + return imported_modules + + def _filename_friendly(name: str) -> str: """ Given a string, return a version that is friendly for use in a filename. @@ -57,7 +87,13 @@ def _filename_friendly(name: str) -> str: Returns: str: A version of the string that is friendly for use in a filename. """ - return name.lower().replace(" ", "_").replace(".", "_") + # replace all non-alphanumeric characters with underscores and convert to lowercase + name = re.sub(r"[^a-zA-Z0-9]", "_", name) + name = name.lower() + # replace any sequence of underscores with a single underscore + name = re.sub(r"_+", "_", name) + + return name # create a runtime context that ensures that dir exists @@ -98,8 +134,7 @@ def remove_if_exists(file): logger.debug(f"File {file} does not exist, nothing to remove") -def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool -) -> None: +def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool) -> None: """ Generate the markdown table, json example, and markdown table file for a decision point. @@ -121,9 +156,7 @@ def dump_decision_point(jsondir: str, dp: SsvcDecisionPoint, overwrite: bool dump_json(basename, dp, jsondir, overwrite) -def dump_json( - basename: str, dp: SsvcDecisionPoint, jsondir: str, overwrite: bool -) -> str: +def dump_json(basename: str, dp: DecisionPoint, jsondir: str, overwrite: bool) -> str: """ Generate the json example for a decision point. @@ -141,15 +174,16 @@ def dump_json( parts = [ jsondir, ] - if dp.namespace != "ssvc": - parts.append(_filename_friendly(dp.namespace)) + parts.append(_filename_friendly(dp.namespace)) + dirname = os.path.join(*parts) + parts.append(filename) json_file = os.path.join(*parts) if overwrite: remove_if_exists(json_file) - with EnsureDirExists(jsondir): + with EnsureDirExists(dirname): try: with open(json_file, "x") as f: f.write(dp.model_dump_json(indent=2)) @@ -158,7 +192,7 @@ def dump_json( logger.warning( f"File {json_file} already exists, use --overwrite to replace" ) - return json_file + return str(json_file) def main(): @@ -189,6 +223,13 @@ def main(): overwrite = args.overwrite jsondir = args.jsondir + find_modules_to_import("./src/ssvc/decision_points", "ssvc.decision_points") + find_modules_to_import("./src/ssvc/outcomes", "ssvc.outcomes") + + # import collections to ensure they are registered too + import ssvc.dp_groups.ssvc.collections # noqa: F401 + import ssvc.dp_groups.cvss.collections # noqa: F401 + # for each decision point: for dp in REGISTERED_DECISION_POINTS: dump_decision_point(jsondir, dp, overwrite) diff --git a/src/ssvc/dp_groups/base.py b/src/ssvc/dp_groups/base.py index e039ba1c..4f496a83 100644 --- a/src/ssvc/dp_groups/base.py +++ b/src/ssvc/dp_groups/base.py @@ -1,9 +1,5 @@ #!/usr/bin/env python -""" -file: base -author: adh -created_at: 9/20/23 4:47 PM -""" + # Copyright (c) 2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE # ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. @@ -23,40 +19,77 @@ # subject to its own license. # DM24-0278 +""" +Provides a DecisionPointGroup object for use in SSVC. +""" + +import itertools +from typing import Generator + from pydantic import BaseModel -from ssvc._mixins import _Base, _Versioned -from ssvc.decision_points.base import SsvcDecisionPoint +from ssvc._mixins import _Base, _SchemaVersioned +from ssvc.decision_points.base import ( + DecisionPoint, + ValueSummary, +) -class SsvcDecisionPointGroup(_Base, _Versioned, BaseModel): +class DecisionPointGroup(_Base, _SchemaVersioned, BaseModel): """ Models a group of decision points. """ - decision_points: list[SsvcDecisionPoint] + decision_points: tuple[DecisionPoint, ...] - def __iter__(self): + def __iter__(self) -> Generator[DecisionPoint, None, None]: """ Allow iteration over the decision points in the group. """ return iter(self.decision_points) - def __len__(self): + def __len__(self) -> int: """ Allow len() to be called on the group. """ - dplist = list(self.decision_points) - l = len(dplist) - return l + return len(self.decision_points) + + @property + def decision_points_dict(self) -> dict[str, DecisionPoint]: + """ + Return a dictionary of decision points keyed by their name. + """ + return {dp.str: dp for dp in self.decision_points} + + @property + def decision_points_str(self) -> list[str]: + """ + Return a list of decision point names. + """ + return list(self.decision_points_dict.keys()) + + def combination_strings(self) -> Generator[tuple[str, ...], None, None]: + """ + Return a list of tuples of the value short strings for all combinations of the decision points. + """ + for combo in self.combinations(): + yield tuple(str(x) for x in combo) + + def combinations(self) -> Generator[tuple[ValueSummary, ...], None, None]: + """ + Return a list of tuples of the value summaries for all combinations of the decision points. + """ + value_tuples = [dp.value_summaries for dp in self.decision_points] + for combo in itertools.product(*value_tuples): + yield combo def get_all_decision_points_from( - *groups: list[SsvcDecisionPointGroup], -) -> list[SsvcDecisionPoint]: + *groups: list[DecisionPointGroup], +) -> tuple[DecisionPoint, ...]: """ - Given a list of SsvcDecisionPointGroup objects, return a list of all - the unique SsvcDecisionPoint objects contained in those groups. + Given a list of DecisionPointGroup objects, return a list of all + the unique DecisionPoint objects contained in those groups. Args: groups (list): A list of SsvcDecisionPointGroup objects. @@ -80,7 +113,7 @@ def get_all_decision_points_from( dps.append(dp) seen.add(key) - return list(dps) + return tuple(dps) def main(): diff --git a/src/ssvc/dp_groups/cvss/collections.py b/src/ssvc/dp_groups/cvss/collections.py index 95d3a4a7..4beca498 100644 --- a/src/ssvc/dp_groups/cvss/collections.py +++ b/src/ssvc/dp_groups/cvss/collections.py @@ -129,7 +129,7 @@ USER_INTERACTION_1, USER_INTERACTION_2, ) -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup # Instantiate the CVSS v1 decision point group _BASE_1 = [ @@ -151,21 +151,21 @@ TARGET_DISTRIBUTION_1, ] -CVSSv1_B = SsvcDecisionPointGroup( +CVSSv1_B = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", decision_points=tuple(_BASE_1), ) -CVSSv1_BT = SsvcDecisionPointGroup( +CVSSv1_BT = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", decision_points=tuple(_BASE_1 + _TEMPORAL_1), ) -CVSSv1_BTE = SsvcDecisionPointGroup( +CVSSv1_BTE = DecisionPointGroup( name="CVSS", version="1.0", description="CVSS v1 decision points", @@ -197,21 +197,21 @@ AVAILABILITY_REQUIREMENT_1, ] -CVSSv2_B = SsvcDecisionPointGroup( +CVSSv2_B = DecisionPointGroup( name="CVSS Version 2 Base Metrics", description="Base metrics for CVSS v2", version="2.0", decision_points=tuple(_BASE_2), ) -CVSSv2_BT = SsvcDecisionPointGroup( +CVSSv2_BT = DecisionPointGroup( name="CVSS Version 2 Base and Temporal Metrics", description="Base and Temporal metrics for CVSS v2", version="2.0", decision_points=tuple(_BASE_2 + _TEMPORAL_2), ) -CVSSv2_BTE = SsvcDecisionPointGroup( +CVSSv2_BTE = DecisionPointGroup( name="CVSS Version 2 Base, Temporal, and Environmental Metrics", description="Base, Temporal, and Environmental metrics for CVSS v2", version="2.0", @@ -246,21 +246,21 @@ ] ) -CVSSv3_B = SsvcDecisionPointGroup( +CVSSv3_B = DecisionPointGroup( name="CVSS Version 3 Base Metrics", description="Base metrics for CVSS v3", version="3.0", decision_points=tuple(_BASE_3), ) -CVSSv3_BT = SsvcDecisionPointGroup( +CVSSv3_BT = DecisionPointGroup( name="CVSS Version 3 Base and Temporal Metrics", description="Base and Temporal metrics for CVSS v3", version="3.0", decision_points=tuple(_BASE_3 + _TEMPORAL_3), ) -CVSSv3_BTE = SsvcDecisionPointGroup( +CVSSv3_BTE = DecisionPointGroup( name="CVSS Version 3 Base, Temporal, and Environmental Metrics", description="Base, Temporal, and Environmental metrics for CVSS v3", version="3.0", @@ -310,7 +310,7 @@ VULNERABILITY_RESPONSE_EFFORT_1, ] # CVSS-B Base metrics -CVSSv4_B = SsvcDecisionPointGroup( +CVSSv4_B = DecisionPointGroup( name="CVSSv4 Base Metrics", description="Base metrics for CVSS v4", version="1.0.0", @@ -318,7 +318,7 @@ ) # CVSS-BE Base and Environmental metrics -CVSSv4_BE = SsvcDecisionPointGroup( +CVSSv4_BE = DecisionPointGroup( name="CVSSv4 Base and Environmental Metrics", description="Base and Environmental metrics for CVSS v4", version="1.0.0", @@ -326,7 +326,7 @@ ) # CVSS-BT Base and Threat metrics -CVSSv4_BT = SsvcDecisionPointGroup( +CVSSv4_BT = DecisionPointGroup( name="CVSSv4 Base and Threat Metrics", description="Base and Threat metrics for CVSS v4", version="1.0.0", @@ -334,21 +334,21 @@ ) # CVSS-BTE -CVSSv4_BTE = SsvcDecisionPointGroup( +CVSSv4_BTE = DecisionPointGroup( name="CVSSv4 Base, Threat, and Environmental Metrics", description="Base, Threat, and Environmental metrics for CVSS v4", version="1.0.0", decision_points=tuple(_BASE_4 + _THREAT_4 + _ENVIRONMENTAL_4), ) -CVSSv4 = SsvcDecisionPointGroup( +CVSSv4 = DecisionPointGroup( name="CVSSv4", description="All decision points for CVSS v4 (including supplemental metrics)", version="1.0.0", decision_points=tuple(_BASE_4 + _THREAT_4 + _ENVIRONMENTAL_4 + _SUPPLEMENTAL_4), ) -CVSSv4_Equivalence_Sets = SsvcDecisionPointGroup( +CVSSv4_Equivalence_Sets = DecisionPointGroup( name="CVSSv4 EQ Sets", description="Equivalence Sets for CVSS v4", version="1.0.0", diff --git a/src/ssvc/dp_groups/ssvc/collections.py b/src/ssvc/dp_groups/ssvc/collections.py index 91e758b4..836eb12b 100644 --- a/src/ssvc/dp_groups/ssvc/collections.py +++ b/src/ssvc/dp_groups/ssvc/collections.py @@ -23,7 +23,7 @@ from ssvc.dp_groups.base import ( - SsvcDecisionPointGroup, + DecisionPointGroup, get_all_decision_points_from, ) from ssvc.dp_groups.ssvc.coordinator_publication import ( @@ -38,7 +38,7 @@ from ssvc.dp_groups.ssvc.supplier import PATCH_DEVELOPER_1, SUPPLIER_2 -SSVCv1 = SsvcDecisionPointGroup( +SSVCv1 = DecisionPointGroup( name="SSVCv1", description="The first version of the SSVC.", version="1.0.0", @@ -46,7 +46,7 @@ PATCH_APPLIER_1, PATCH_DEVELOPER_1 ), ) -SSVCv2 = SsvcDecisionPointGroup( +SSVCv2 = DecisionPointGroup( name="SSVCv2", description="The second version of the SSVC.", version="2.0.0", @@ -54,7 +54,7 @@ COORDINATOR_PUBLICATION_1, COORDINATOR_TRIAGE_1, DEPLOYER_2, SUPPLIER_2 ), ) -SSVCv2_1 = SsvcDecisionPointGroup( +SSVCv2_1 = DecisionPointGroup( name="SSVCv2.1", description="The second version of the SSVC.", version="2.1.0", @@ -63,10 +63,12 @@ ), ) +VERSIONS = (SSVCv1, SSVCv2, SSVCv2_1) +LATEST = VERSIONS[-1] def main(): - for dpg in [SSVCv1, SSVCv2, SSVCv2_1]: - print(dpg.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/coordinator_publication.py b/src/ssvc/dp_groups/ssvc/coordinator_publication.py index 4cddb5c0..77f773d0 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_publication.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_publication.py @@ -23,13 +23,13 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.public_value_added import PUBLIC_VALUE_ADDED_1 -from ssvc.decision_points.supplier_involvement import SUPPLIER_INVOLVEMENT_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.public_value_added import PUBLIC_VALUE_ADDED_1 +from ssvc.decision_points.ssvc.supplier_involvement import SUPPLIER_INVOLVEMENT_1 +from ssvc.dp_groups.base import DecisionPointGroup -COORDINATOR_PUBLICATION_1 = SsvcDecisionPointGroup( +COORDINATOR_PUBLICATION_1 = DecisionPointGroup( name="Coordinator Publication", description="The decision points used by the coordinator during publication.", version="1.0.0", @@ -49,9 +49,13 @@ - Public Value Added v1.0.0 """ +VERSIONS = (COORDINATOR_PUBLICATION_1,) +LATEST = VERSIONS[-1] + def main(): - print(COORDINATOR_PUBLICATION_1.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/coordinator_triage.py b/src/ssvc/dp_groups/ssvc/coordinator_triage.py index bc65e52e..cd625ef4 100644 --- a/src/ssvc/dp_groups/ssvc/coordinator_triage.py +++ b/src/ssvc/dp_groups/ssvc/coordinator_triage.py @@ -23,20 +23,19 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.automatable import AUTOMATABLE_2 -from ssvc.decision_points.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 -from ssvc.decision_points.report_credibility import REPORT_CREDIBILITY_1 -from ssvc.decision_points.report_public import REPORT_PUBLIC_1 -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.supplier_cardinality import SUPPLIER_CARDINALITY_1 -from ssvc.decision_points.supplier_contacted import SUPPLIER_CONTACTED_1 -from ssvc.decision_points.supplier_engagement import SUPPLIER_ENGAGEMENT_1 -from ssvc.decision_points.utility import UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc.public_safety_impact import PUBLIC_SAFETY_IMPACT_2 +from ssvc.decision_points.ssvc.report_credibility import REPORT_CREDIBILITY_1 +from ssvc.decision_points.ssvc.report_public import REPORT_PUBLIC_1 +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.supplier_cardinality import SUPPLIER_CARDINALITY_1 +from ssvc.decision_points.ssvc.supplier_contacted import SUPPLIER_CONTACTED_1 +from ssvc.decision_points.ssvc.supplier_engagement import SUPPLIER_ENGAGEMENT_1 +from ssvc.decision_points.ssvc.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 +from ssvc.dp_groups.base import DecisionPointGroup - -COORDINATOR_TRIAGE_1 = SsvcDecisionPointGroup( +COORDINATOR_TRIAGE_1 = DecisionPointGroup( name="Coordinator Triage", description="The decision points used by the coordinator during triage.", version="1.0.0", @@ -70,9 +69,13 @@ - Safety Impact v1.0.0 """ +VERSIONS = (COORDINATOR_TRIAGE_1,) +LATEST = VERSIONS[-1] + def main(): - print(COORDINATOR_TRIAGE_1.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/deployer.py b/src/ssvc/dp_groups/ssvc/deployer.py index b0c52b23..4473746c 100644 --- a/src/ssvc/dp_groups/ssvc/deployer.py +++ b/src/ssvc/dp_groups/ssvc/deployer.py @@ -24,32 +24,32 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.automatable import AUTOMATABLE_2 -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.human_impact import HUMAN_IMPACT_2 -from ssvc.decision_points.mission_impact import ( +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.human_impact import HUMAN_IMPACT_2 +from ssvc.decision_points.ssvc.mission_impact import ( MISSION_IMPACT_1, MISSION_IMPACT_2, ) -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.system_exposure import ( +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.system_exposure import ( SYSTEM_EXPOSURE_1, SYSTEM_EXPOSURE_1_0_1, ) -from ssvc.decision_points.utility import UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.decision_points.ssvc.utility import UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 +from ssvc.dp_groups.base import DecisionPointGroup -PATCH_APPLIER_1 = SsvcDecisionPointGroup( +PATCH_APPLIER_1 = DecisionPointGroup( name="SSVC Patch Applier", description="The decision points used by the patch applier.", version="1.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1, MISSION_IMPACT_1, SAFETY_IMPACT_1, - ], + ), ) """ In SSVC v1, Patch Applier v1 represents the decision points used by the patch applier. @@ -66,11 +66,11 @@ DEPLOYER_1 = PATCH_APPLIER_1 # SSVC v2 -DEPLOYER_2 = SsvcDecisionPointGroup( +DEPLOYER_2 = DecisionPointGroup( name="SSVC Deployer", description="The decision points used by the deployer.", version="2.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1_0_1, MISSION_IMPACT_1, @@ -79,7 +79,7 @@ AUTOMATABLE_2, VALUE_DENSITY_1, HUMAN_IMPACT_2, - ], + ), ) """ Deployer v2.0.0 is renamed from Patch Applier v1.0.0. @@ -100,7 +100,7 @@ - Human Impact v1.0.0 is added, which depends on Mission Impact v1.0.0 and Safety Impact v1.0.0 """ -DEPLOYER_3 = SsvcDecisionPointGroup( +DEPLOYER_3 = DecisionPointGroup( name="SSVC Deployer", description="The decision points used by the deployer.", version="3.0.0", @@ -128,11 +128,13 @@ - Mission Impact v1.0.0 -> v2.0.0 """ +VERSIONS = (PATCH_APPLIER_1, DEPLOYER_2, DEPLOYER_3) +LATEST = VERSIONS[-1] + def main(): - print(PATCH_APPLIER_1.model_dump_json(indent=2)) - print(DEPLOYER_2.model_dump_json(indent=2)) - print(DEPLOYER_3.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/dp_groups/ssvc/supplier.py b/src/ssvc/dp_groups/ssvc/supplier.py index de94b215..f7a73c13 100644 --- a/src/ssvc/dp_groups/ssvc/supplier.py +++ b/src/ssvc/dp_groups/ssvc/supplier.py @@ -24,15 +24,15 @@ # subject to its own license. # DM24-0278 -from ssvc.decision_points.automatable import AUTOMATABLE_2, VIRULENCE_1 -from ssvc.decision_points.exploitation import EXPLOITATION_1 -from ssvc.decision_points.safety_impact import SAFETY_IMPACT_1 -from ssvc.decision_points.technical_impact import TECHNICAL_IMPACT_1 -from ssvc.decision_points.utility import UTILITY_1, UTILITY_1_0_1 -from ssvc.decision_points.value_density import VALUE_DENSITY_1 -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2, VIRULENCE_1 +from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 +from ssvc.decision_points.ssvc.safety_impact import SAFETY_IMPACT_1 +from ssvc.decision_points.ssvc.technical_impact import TECHNICAL_IMPACT_1 +from ssvc.decision_points.ssvc.utility import UTILITY_1, UTILITY_1_0_1 +from ssvc.decision_points.ssvc.value_density import VALUE_DENSITY_1 +from ssvc.dp_groups.base import DecisionPointGroup -PATCH_DEVELOPER_1 = SsvcDecisionPointGroup( +PATCH_DEVELOPER_1 = DecisionPointGroup( name="SSVC Patch Developer", description="The decision points used by the patch developer.", version="1.0.0", @@ -62,18 +62,18 @@ SUPPLIER_1 = PATCH_DEVELOPER_1 # SSVC v2 renamed to SSVC Supplier -SUPPLIER_2 = SsvcDecisionPointGroup( +SUPPLIER_2 = DecisionPointGroup( name="SSVC Supplier", description="The decision points used by the supplier.", version="2.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, UTILITY_1_0_1, TECHNICAL_IMPACT_1, AUTOMATABLE_2, VALUE_DENSITY_1, SAFETY_IMPACT_1, - ], + ), ) """ In SSVC v2, Supplier v2 represents the decision points used by the supplier. @@ -95,10 +95,13 @@ - Public Safety Impact v1.0.0 added, which subsumes Safety Impact v1.0.0 """ +VERSIONS = (PATCH_DEVELOPER_1, SUPPLIER_2) +LATEST = VERSIONS[-1] + def main(): - print(PATCH_DEVELOPER_1.model_dump_json(indent=2)) - print(SUPPLIER_2.model_dump_json(indent=2)) + for version in VERSIONS: + print(version.model_dump_json(indent=2)) if __name__ == "__main__": diff --git a/src/ssvc/namespaces.py b/src/ssvc/namespaces.py index 4c34260c..56cb3ee4 100644 --- a/src/ssvc/namespaces.py +++ b/src/ssvc/namespaces.py @@ -79,6 +79,7 @@ class NameSpace(StrEnum): # when used in a StrEnum, auto() assigns the lowercase name of the member as the value SSVC = auto() CVSS = auto() + CISA = auto() @classmethod def validate(cls, value: str) -> str: diff --git a/src/ssvc/outcomes/__init__.py b/src/ssvc/outcomes/__init__.py index 99cfbb7c..499a1243 100644 --- a/src/ssvc/outcomes/__init__.py +++ b/src/ssvc/outcomes/__init__.py @@ -16,3 +16,15 @@ # This Software includes and/or makes use of Third-Party Software each # subject to its own license. # DM24-0278 +""" +Provides SSVC outcome group objects. + +SSVC outcome groups are functionally equivalent to Decision Points. +The only difference is that Outcome Groups are primarily intended to be used +as the outputs of a decision, whereas Decision Points are the inputs to a decision. +However, there are use cases where an outcome of one decision may feed into another +decision, so the distinction is somewhat arbitrary. Hence, we chose to use the same +data structure for both. + +Outcome groups are organized by namespace. +""" diff --git a/src/ssvc/outcomes/base.py b/src/ssvc/outcomes/base.py index 894257b1..82c9da08 100644 --- a/src/ssvc/outcomes/base.py +++ b/src/ssvc/outcomes/base.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python -""" -Provides outcome group and outcome value classes for SSVC. -""" # Copyright (c) 2023-2025 Carnegie Mellon University. # NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE # ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. @@ -20,37 +16,17 @@ # This Software includes and/or makes use of Third-Party Software each # subject to its own license. # DM24-0278 +""" +Provides outcome group and outcome value classes for SSVC. +""" -from pydantic import BaseModel - -from ssvc._mixins import _Base, _Keyed, _Versioned - - -class OutcomeValue(_Base, _Keyed, BaseModel): - """ - Models a single value option for an SSVC outcome. - """ - - -class OutcomeGroup(_Base, _Versioned, BaseModel): - """ - Models an outcome group. - """ - - outcomes: list[OutcomeValue] - - def __iter__(self): - """ - Allow iteration over the outcomes in the group. - """ - return iter(self.outcomes) - - def __len__(self): - """ - Allow len() to be called on the group. - """ - olist = list(self.outcomes) - l = len(olist) - return l +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint +from ssvc.decision_points.cvss.base import CvssDecisionPoint +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint - # register all instances +OutcomeValue = DecisionPointValue +OutcomeGroup = DecisionPoint +SsvcOutcomeGroup = SsvcDecisionPoint +CvssOutcomeGroup = CvssDecisionPoint +CisaOutcomeGroup = CisaDecisionPoint diff --git a/src/ssvc/outcomes/cisa/__init__.py b/src/ssvc/outcomes/cisa/__init__.py new file mode 100644 index 00000000..ae8acaa4 --- /dev/null +++ b/src/ssvc/outcomes/cisa/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the cisa namespace +""" +from .scoring import LATEST as CISA_SCORING diff --git a/src/ssvc/outcomes/cisa/scoring.py b/src/ssvc/outcomes/cisa/scoring.py new file mode 100644 index 00000000..6b64bac1 --- /dev/null +++ b/src/ssvc/outcomes/cisa/scoring.py @@ -0,0 +1,81 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the CISA Levels outcome group for use in SSVC. +""" + +from ssvc.decision_points.base import DecisionPointValue +from ssvc.decision_points.cisa.base import CisaDecisionPoint +from ssvc.decision_points.helpers import print_versions_and_diffs + +_TRACK = DecisionPointValue( + name="Track", + key="T", + description="The vulnerability does not require action at this time. " + "The organization would continue to track the vulnerability and reassess it if new information becomes available. " + "CISA recommends remediating Track vulnerabilities within standard update timelines.", +) + +_TRACK_STAR = DecisionPointValue( + name="Track*", + key="T*", + description="The vulnerability contains specific characteristics that may require closer monitoring for changes. " + "CISA recommends remediating Track* vulnerabilities within standard update timelines.", +) + +_ATTEND = DecisionPointValue( + name="Attend", + key="A", + description="The vulnerability requires attention from the organization's internal, supervisory-level individuals. " + "Necessary actions may include requesting assistance or information about the vulnerability and may involve publishing a notification, either internally and/or externally, about the vulnerability. " + "CISA recommends remediating Attend vulnerabilities sooner than standard update timelines.", +) + +_ACT = DecisionPointValue( + name="Act", + key="A", + description="The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. " + "Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. " + "Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. " + "CISA recommends remediating Act vulnerabilities as soon as possible.", +) + +CISA = CisaDecisionPoint( + name="CISA Levels", + key="CISA", + description="The CISA outcome group. " + "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", + version="1.0.0", + values=( + _TRACK, + _TRACK_STAR, + _ATTEND, + _ACT, + ), +) +""" +The CISA outcome group. Based on CISA's customizations of the SSVC model. +See https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc +""" + + +VERSIONS = (CISA,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/cvss/__init__.py b/src/ssvc/outcomes/cvss/__init__.py new file mode 100644 index 00000000..6f4e34fc --- /dev/null +++ b/src/ssvc/outcomes/cvss/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides outcome group objects in the cvss namespace +""" +from .lmhc import LATEST as LMHC diff --git a/src/ssvc/outcomes/cvss/lmhc.py b/src/ssvc/outcomes/cvss/lmhc.py new file mode 100644 index 00000000..673c9794 --- /dev/null +++ b/src/ssvc/outcomes/cvss/lmhc.py @@ -0,0 +1,55 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.cvss.base import CvssDecisionPoint +from ssvc.decision_points.helpers import print_versions_and_diffs + +_NONE = DecisionPointValue(name="None", key="N", description="None (0.0)") + +_LOW = DecisionPointValue(name="Low", key="L", description="Low (0.1-3.9)") + +_MEDIUM = DecisionPointValue(name="Medium", key="M", description="Medium (4.0-6.9)") + +_HIGH = DecisionPointValue(name="High", key="H", description="High (7.0-8.9)") + +_CRITICAL = DecisionPointValue( + name="Critical", key="C", description="Critical (9.0-10.0)" +) + +LMHC = CvssDecisionPoint( + name="CVSS Qualitative Severity Rating Scale", + key="CVSS", + description="The CVSS Qualitative Severity Rating Scale group.", + version="1.0.0", + values=( + _NONE, + _LOW, + _MEDIUM, + _HIGH, + _CRITICAL, + ), +) +""" +The CVSS Qualitative Severity (N,L,M,H,C) Rating Scale outcome group. +""" + +VERSIONS = (LMHC,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/groups.py b/src/ssvc/outcomes/groups.py index c6da9c25..e69de29b 100644 --- a/src/ssvc/outcomes/groups.py +++ b/src/ssvc/outcomes/groups.py @@ -1,217 +0,0 @@ -#!/usr/bin/env python -""" -Provides a set of outcome groups for use in SSVC. -""" -# Copyright (c) 2023-2025 Carnegie Mellon University. -# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE -# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. -# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, -# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT -# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR -# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE -# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE -# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM -# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. -# Licensed under a MIT (SEI)-style license, please see LICENSE or contact -# permission@sei.cmu.edu for full terms. -# [DISTRIBUTION STATEMENT A] This material has been approved for -# public release and unlimited distribution. Please see Copyright notice -# for non-US Government use and distribution. -# This Software includes and/or makes use of Third-Party Software each -# subject to its own license. -# DM24-0278 - -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue - -# Note: Outcome Groups must be defined in ascending order. - - -DSOI = OutcomeGroup( - name="Defer, Scheduled, Out-of-Cycle, Immediate", - description="The original SSVC outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Defer", key="D", description="Defer"), - OutcomeValue(name="Scheduled", key="S", description="Scheduled"), - OutcomeValue(name="Out-of-Cycle", key="O", description="Out-of-Cycle"), - OutcomeValue(name="Immediate", key="I", description="Immediate"), - ), -) -""" -The original SSVC outcome group. -""" - -PUBLISH = OutcomeGroup( - name="Publish, Do Not Publish", - description="The publish outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue( - name="Do Not Publish", key="N", description="Do Not Publish" - ), - OutcomeValue(name="Publish", key="P", description="Publish"), - ), -) -""" -The publish outcome group. -""" - -COORDINATE = OutcomeGroup( - name="Decline, Track, Coordinate", - description="The coordinate outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Decline", key="D", description="Decline"), - OutcomeValue(name="Track", key="T", description="Track"), - OutcomeValue(name="Coordinate", key="C", description="Coordinate"), - ), -) -""" -The coordinate outcome group. -""" - -MOSCOW = OutcomeGroup( - name="Must, Should, Could, Won't", - description="The Moscow outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Won't", key="W", description="Won't"), - OutcomeValue(name="Could", key="C", description="Could"), - OutcomeValue(name="Should", key="S", description="Should"), - OutcomeValue(name="Must", key="M", description="Must"), - ), -) -""" -The MoSCoW outcome group. -""" - -EISENHOWER = OutcomeGroup( - name="Do, Schedule, Delegate, Delete", - description="The Eisenhower outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Delete", key="D", description="Delete"), - OutcomeValue(name="Delegate", key="G", description="Delegate"), - OutcomeValue(name="Schedule", key="S", description="Schedule"), - OutcomeValue(name="Do", key="O", description="Do"), - ), -) -""" -The Eisenhower outcome group. -""" - -CVSS = OutcomeGroup( - name="CVSS Levels", - description="The CVSS outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Low", key="L", description="Low"), - OutcomeValue(name="Medium", key="M", description="Medium"), - OutcomeValue(name="High", key="H", description="High"), - OutcomeValue(name="Critical", key="C", description="Critical"), - ), -) -""" -The CVSS outcome group. -""" - -CISA = OutcomeGroup( - name="CISA Levels", - description="The CISA outcome group. " - "CISA uses its own SSVC decision tree model to prioritize relevant vulnerabilities into four possible decisions: Track, Track*, Attend, and Act.", - version="1.0.0", - outcomes=( - OutcomeValue( - name="Track", - key="T", - description="The vulnerability does not require action at this time. " - "The organization would continue to track the vulnerability and reassess it if new information becomes available. " - "CISA recommends remediating Track vulnerabilities within standard update timelines.", - ), - OutcomeValue( - name="Track*", - key="T*", - description="The vulnerability contains specific characteristics that may require closer monitoring for changes. " - "CISA recommends remediating Track* vulnerabilities within standard update timelines.", - ), - OutcomeValue( - name="Attend", - key="A", - description="The vulnerability requires attention from the organization's internal, supervisory-level individuals. " - "Necessary actions may include requesting assistance or information about the vulnerability and may involve publishing a notification, either internally and/or externally, about the vulnerability. " - "CISA recommends remediating Attend vulnerabilities sooner than standard update timelines.", - ), - OutcomeValue( - name="Act", - key="A", - description="The vulnerability requires attention from the organization's internal, supervisory-level and leadership-level individuals. " - "Necessary actions include requesting assistance or information about the vulnerability, as well as publishing a notification either internally and/or externally. " - "Typically, internal groups would meet to determine the overall response and then execute agreed upon actions. " - "CISA recommends remediating Act vulnerabilities as soon as possible.", - ), - ), -) -""" -The CISA outcome group. Based on CISA's customizations of the SSVC model. -See https://www.cisa.gov/stakeholder-specific-vulnerability-categorization-ssvc -""" - -YES_NO = OutcomeGroup( - name="Yes, No", - description="The Yes/No outcome group.", - version="1.0.0", - outcomes=( - OutcomeValue(name="No", key="N", description="No"), - OutcomeValue(name="Yes", key="Y", description="Yes"), - ), -) -""" -The Yes/No outcome group. -""" - -VALUE_COMPLEXITY = OutcomeGroup( - name="Value, Complexity", - description="The Value/Complexity outcome group.", - version="1.0.0", - outcomes=( - # drop, reconsider later, easy win, do first - OutcomeValue(name="Drop", key="D", description="Drop"), - OutcomeValue( - name="Reconsider Later", key="R", description="Reconsider Later" - ), - OutcomeValue(name="Easy Win", key="E", description="Easy Win"), - OutcomeValue(name="Do First", key="F", description="Do First"), - ), -) -""" -The Value/Complexity outcome group. -""" - -THE_PARANOIDS = OutcomeGroup( - name="theParanoids", - description="PrioritizedRiskRemediation outcome group based on TheParanoids.", - version="1.0.0", - outcomes=( - OutcomeValue(name="Track 5", key="5", description="Track"), - OutcomeValue( - name="Track Closely 4", key="4", description="Track Closely" - ), - OutcomeValue(name="Attend 3", key="3", description="Attend"), - OutcomeValue(name="Attend 2", key="2", description="Attend"), - OutcomeValue(name="Act 1", key="1", description="Act"), - OutcomeValue(name="Act ASAP 0", key="0", description="Act ASAP"), - ), -) -""" -Outcome group based on TheParanoids' PrioritizedRiskRemediation. -Their model is a 6-point scale, with 0 being the most urgent and 5 being the least. -See https://github.com/theparanoids/PrioritizedRiskRemediation -""" - - -def main(): - pass - - -if __name__ == "__main__": - main() diff --git a/src/ssvc/outcomes/ssvc/__init__.py b/src/ssvc/outcomes/ssvc/__init__.py new file mode 100644 index 00000000..55937544 --- /dev/null +++ b/src/ssvc/outcomes/ssvc/__init__.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +""" +Provides outcome group objects in the ssvc namespace +""" + +from .coordinate import LATEST as COORDINATE +from .dsoi import LATEST as DSOI +from .publish import LATEST as PUBLISH diff --git a/src/ssvc/outcomes/ssvc/coordinate.py b/src/ssvc/outcomes/ssvc/coordinate.py new file mode 100644 index 00000000..136d987e --- /dev/null +++ b/src/ssvc/outcomes/ssvc/coordinate.py @@ -0,0 +1,53 @@ +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DECLINE = DecisionPointValue(name="Decline", key="D", description="Decline") + +_TRACK = DecisionPointValue(name="Track", key="T", description="Track") + +_COORDINATE = DecisionPointValue(name="Coordinate", key="C", description="Coordinate") + +COORDINATE = SsvcDecisionPoint( + name="Decline, Track, Coordinate", + key="COORDINATE", + description="The coordinate outcome group.", + version="1.0.0", + values=( + _DECLINE, + _TRACK, + _COORDINATE, + ), +) +""" +The coordinate outcome group. +""" + +VERSIONS = (COORDINATE,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc/dsoi.py b/src/ssvc/outcomes/ssvc/dsoi.py new file mode 100644 index 00000000..cf477abb --- /dev/null +++ b/src/ssvc/outcomes/ssvc/dsoi.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 +""" +Provides the defer, scheduled, out-of-cycle, immediate outcome group for use in SSVC. +""" +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DEFER = DecisionPointValue(name="Defer", key="D", description="Defer") + +_SCHEDULED = DecisionPointValue(name="Scheduled", key="S", description="Scheduled") + +_OUT_OF_CYCLE = DecisionPointValue( + name="Out-of-Cycle", key="O", description="Out-of-Cycle" +) + +_IMMEDIATE = DecisionPointValue(name="Immediate", key="I", description="Immediate") + +DSOI = SsvcDecisionPoint( + name="Defer, Scheduled, Out-of-Cycle, Immediate", + key="DSOI", + description="The original SSVC outcome group.", + version="1.0.0", + values=( + _DEFER, + _SCHEDULED, + _OUT_OF_CYCLE, + _IMMEDIATE, + ), +) +""" +The original SSVC outcome group. +""" + + +VERSIONS = (DSOI,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/ssvc/publish.py b/src/ssvc/outcomes/ssvc/publish.py new file mode 100644 index 00000000..bcc2eb13 --- /dev/null +++ b/src/ssvc/outcomes/ssvc/publish.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 + +from ssvc.decision_points.base import DecisionPointValue as DecisionPointValue +from ssvc.decision_points.helpers import print_versions_and_diffs +from ssvc.decision_points.ssvc.base import SsvcDecisionPoint + +_DO_NOT_PUBLISH = DecisionPointValue( + name="Do Not Publish", key="N", description="Do Not Publish" +) + +_PUBLISH = DecisionPointValue(name="Publish", key="P", description="Publish") + +PUBLISH = SsvcDecisionPoint( + name="Publish, Do Not Publish", + key="PUBLISH", + description="The publish outcome group.", + version="1.0.0", + values=( + _DO_NOT_PUBLISH, + _PUBLISH, + ), +) +""" +The publish outcome group. +""" + +VERSIONS = (PUBLISH,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/__init__.py b/src/ssvc/outcomes/x_basic/__init__.py new file mode 100644 index 00000000..73ac8ef9 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the `x_basic` namespace +""" + +from .ike import LATEST as EISENHOWER +from .mscw import LATEST as MSCW +from .value_complexity import LATEST as VALUE_COMPLEXITY +from .yn import LATEST as YES_NO diff --git a/src/ssvc/outcomes/x_basic/ike.py b/src/ssvc/outcomes/x_basic/ike.py new file mode 100644 index 00000000..0a56e9ee --- /dev/null +++ b/src/ssvc/outcomes/x_basic/ike.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the Eisenhower outcome group for the `x_basic` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_DELETE = DecisionPointValue(name="Delete", key="D", description="Delete") + +_DELEGATE = DecisionPointValue(name="Delegate", key="G", description="Delegate") + +_SCHEDULE = DecisionPointValue(name="Schedule", key="S", description="Schedule") + +_DO = DecisionPointValue(name="Do", key="O", description="Do") + +EISENHOWER = DecisionPoint( + name="Do, Schedule, Delegate, Delete", + key="IKE", + description="The Eisenhower outcome group.", + namespace="x_basic", + version="1.0.0", + values=( + _DELETE, + _DELEGATE, + _SCHEDULE, + _DO, + ), +) +""" +The Eisenhower outcome group. +""" + +VERSIONS = (EISENHOWER,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/mscw.py b/src/ssvc/outcomes/x_basic/mscw.py new file mode 100644 index 00000000..b74ef04e --- /dev/null +++ b/src/ssvc/outcomes/x_basic/mscw.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the MSCW (Must, Should, Could, Won't) outcome group for the `x_basic` namespace +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_WONT = DecisionPointValue(name="Won't", key="W", description="Won't") + +_COULD = DecisionPointValue(name="Could", key="C", description="Could") + +_SHOULD = DecisionPointValue(name="Should", key="S", description="Should") + +_MUST = DecisionPointValue(name="Must", key="M", description="Must") + +MSCW = DecisionPoint( + name="MoSCoW", + key="MSCW", + description="The MoSCoW (Must, Should, Could, Won't) outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _WONT, + _COULD, + _SHOULD, + _MUST, + ), +) +""" +The MoSCoW outcome group. +""" + +VERSIONS = (MSCW,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/value_complexity.py b/src/ssvc/outcomes/x_basic/value_complexity.py new file mode 100644 index 00000000..08a61eb6 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/value_complexity.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides the Value/Complexity outcome group for the `x_basic` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_DROP = DecisionPointValue(name="Drop", key="D", description="Drop") + +_RECONSIDER = DecisionPointValue( + name="Reconsider Later", key="R", description="Reconsider Later" +) + +_EASY_WIN = DecisionPointValue(name="Easy Win", key="E", description="Easy Win") + +_DO_FIRST = DecisionPointValue(name="Do First", key="F", description="Do First") + +VALUE_COMPLEXITY = DecisionPoint( + name="Value, Complexity", + key="VALUE_COMPLEXITY", + description="The Value/Complexity outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _DROP, + _RECONSIDER, + _EASY_WIN, + _DO_FIRST, + ), +) +""" +The Value/Complexity outcome group. +""" + +VERSIONS = (VALUE_COMPLEXITY,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_basic/yn.py b/src/ssvc/outcomes/x_basic/yn.py new file mode 100644 index 00000000..39dddfb2 --- /dev/null +++ b/src/ssvc/outcomes/x_basic/yn.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_NO = DecisionPointValue(name="No", key="N", description="No") + +_YES = DecisionPointValue(name="Yes", key="Y", description="Yes") + +YES_NO = DecisionPoint( + name="Yes, No", + key="YN", + description="The Yes/No outcome group.", + version="1.0.0", + namespace="x_basic", + values=( + _NO, + _YES, + ), +) +""" +The Yes/No outcome group. +""" + +VERSIONS = (YES_NO,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/outcomes/x_community/__init__.py b/src/ssvc/outcomes/x_community/__init__.py new file mode 100644 index 00000000..215c5355 --- /dev/null +++ b/src/ssvc/outcomes/x_community/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides SSVC outcome groups for the `x_community` namespace. +""" + +from .paranoids import LATEST as THE_PARANOIDS diff --git a/src/ssvc/outcomes/x_community/paranoids.py b/src/ssvc/outcomes/x_community/paranoids.py new file mode 100644 index 00000000..2a0f7f8d --- /dev/null +++ b/src/ssvc/outcomes/x_community/paranoids.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University + +""" +Provides a decision point for the `x_community` namespace. +""" + +from ssvc.decision_points.base import ( + DecisionPoint, + DecisionPointValue as DecisionPointValue, +) +from ssvc.decision_points.helpers import print_versions_and_diffs + +_TRACK_5 = DecisionPointValue(name="Track 5", key="5", description="Track") + +_TRACK_CLOSELY_4 = DecisionPointValue( + name="Track Closely 4", key="4", description="Track Closely" +) + +_ATTEND_3 = DecisionPointValue(name="Attend 3", key="3", description="Attend") + +_ATTEND_2 = DecisionPointValue(name="Attend 2", key="2", description="Attend") + +_ACT_1 = DecisionPointValue(name="Act 1", key="1", description="Act") + +_ACT_ASAP_0 = DecisionPointValue(name="Act ASAP 0", key="0", description="Act ASAP") + +THE_PARANOIDS = DecisionPoint( + name="theParanoids", + key="PARANOIDS", + description="PrioritizedRiskRemediation outcome group based on TheParanoids.", + namespace="x_community", + version="1.0.0", + values=( + _TRACK_5, + _TRACK_CLOSELY_4, + _ATTEND_3, + _ATTEND_2, + _ACT_1, + _ACT_ASAP_0, + ), +) +""" +Outcome group based on TheParanoids' PrioritizedRiskRemediation. +Their model is a 6-point scale, with 0 being the most urgent and 5 being the least. +See https://github.com/theparanoids/PrioritizedRiskRemediation +""" + +VERSIONS = (THE_PARANOIDS,) +LATEST = VERSIONS[-1] + + +def main(): + print_versions_and_diffs(VERSIONS) + + +if __name__ == "__main__": + main() diff --git a/src/ssvc/policy_generator.py b/src/ssvc/policy_generator.py index 455ded12..b99b4b8e 100644 --- a/src/ssvc/policy_generator.py +++ b/src/ssvc/policy_generator.py @@ -30,7 +30,7 @@ import pandas as pd from ssvc import csv_analyzer -from ssvc.dp_groups.base import SsvcDecisionPointGroup +from ssvc.dp_groups.base import DecisionPointGroup from ssvc.outcomes.base import OutcomeGroup logger = logging.getLogger(__name__) @@ -51,8 +51,8 @@ class PolicyGenerator: def __init__( self, - dp_group: SsvcDecisionPointGroup = None, - outcomes: OutcomeGroup = None, + dp_group: DecisionPointGroup, + outcomes: OutcomeGroup, outcome_weights: list[float] = None, validate: bool = False, ): @@ -72,7 +72,7 @@ def __init__( if dp_group is None: raise ValueError("dp_group is required") else: - self.dpg: SsvcDecisionPointGroup = dp_group + self.dpg: DecisionPointGroup = dp_group if outcomes is None: raise ValueError("outcomes is required") @@ -92,9 +92,7 @@ def __init__( # validate that the outcome weights sum to 1.0 total = sum(outcome_weights) if not math.isclose(total, 1.0): - raise ValueError( - f"Outcome weights must sum to 1.0, but sum to {total}" - ) + raise ValueError(f"Outcome weights must sum to 1.0, but sum to {total}") self.outcome_weights = outcome_weights logger.debug(f"Outcome weights: {self.outcome_weights}") @@ -171,14 +169,14 @@ def _create_policy(self): row = {} for i in range(len(node)): # turn the numerical indexes back into decision point names - col1 = f"{self.dpg.decision_points[i].name}" - row[col1] = self.dpg.decision_points[i].values[node[i]].name + col1 = f"{self.dpg.decision_points[i].str}" + row[col1] = self.dpg.decision_points[i].value_summaries_str[node[i]] # numerical values - col2 = f"idx_{self.dpg.decision_points[i].name}" + col2 = f"idx_{self.dpg.decision_points[i].str}" row[col2] = node[i] oc_idx = self.G.nodes[node]["outcome"] - row["outcome"] = self.outcomes.outcomes[oc_idx].name + row["outcome"] = self.outcomes.value_summaries_str[oc_idx] row["idx_outcome"] = oc_idx rows.append(row) @@ -187,9 +185,12 @@ def _create_policy(self): def clean_policy(self) -> pd.DataFrame: df = self.policy.copy() + # rename "outcome" column to outcome group name + df = df.rename(columns={"outcome": self.outcomes.str}) print_cols = [c for c in df.columns if not c.startswith("idx_")] - for c in print_cols: - df[c] = df[c].str.lower() + + # for c in print_cols: + # df[c] = df[c].str.lower() return pd.DataFrame(df[print_cols]) @@ -203,22 +204,20 @@ def emit_policy(self) -> None: def _assign_outcomes(self): node_count = len(self.G.nodes) - outcomes = [outcome.name for outcome in self.outcomes.outcomes] + outcomes = [outcome.name for outcome in self.outcomes.values] logger.debug(f"Outcomes: {outcomes}") layers = list(nx.topological_generations(self.G)) logger.debug(f"Layer count: {len(layers)}") logger.debug(f"Layer sizes: {[len(layer) for layer in layers]}") - outcome_counts = [ - round(node_count * weight) for weight in self.outcome_weights - ] + outcome_counts = [round(node_count * weight) for weight in self.outcome_weights] toposort = list(nx.topological_sort(self.G)) logger.debug(f"Toposort: {toposort[:4]}...{toposort[-4:]}") outcome_idx = 0 - assigned_counts = [0 for _ in self.outcomes.outcomes] + assigned_counts = [0 for _ in self.outcomes.values] for node in toposort: # step through the nodes in topological order # and assign outcomes to each node @@ -301,15 +300,11 @@ def _confirm_topological_order(self, node_order: list) -> None: # all nodes must be in the graph for node in node_order: if node not in self.G.nodes: - raise ValueError( - f"Node order contains node {node} not in the graph" - ) + raise ValueError(f"Node order contains node {node} not in the graph") for node in self.G.nodes: if node not in node_order: - raise ValueError( - f"Graph contains node {node} not in the node order" - ) + raise ValueError(f"Graph contains node {node} not in the node order") node_idx = {node: i for i, node in enumerate(node_order)} @@ -341,11 +336,11 @@ def _is_topological_order(self, node_order: list) -> bool: def main(): - from ssvc.decision_points.automatable import AUTOMATABLE_2 - from ssvc.decision_points.exploitation import EXPLOITATION_1 - from ssvc.decision_points.human_impact import HUMAN_IMPACT_2 - from ssvc.decision_points.system_exposure import SYSTEM_EXPOSURE_1_0_1 - from ssvc.outcomes.groups import DSOI + from ssvc.decision_points.ssvc.automatable import AUTOMATABLE_2 + from ssvc.decision_points.ssvc.exploitation import EXPLOITATION_1 + from ssvc.decision_points.ssvc.human_impact import HUMAN_IMPACT_2 + from ssvc.decision_points.ssvc.system_exposure import SYSTEM_EXPOSURE_1_0_1 + from ssvc.outcomes.ssvc.dsoi import DSOI # set up logging logger = logging.getLogger() @@ -353,16 +348,16 @@ def main(): hdlr = logging.StreamHandler() logger.addHandler(hdlr) - dpg = SsvcDecisionPointGroup( + dpg = DecisionPointGroup( name="Dummy Decision Point Group", description="Dummy decision point group", version="1.0.0", - decision_points=[ + decision_points=( EXPLOITATION_1, SYSTEM_EXPOSURE_1_0_1, AUTOMATABLE_2, HUMAN_IMPACT_2, - ], + ), ) with PolicyGenerator( diff --git a/src/test/decision_points/__init__.py b/src/test/decision_points/__init__.py new file mode 100644 index 00000000..5c215061 --- /dev/null +++ b/src/test/decision_points/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.decision_points. +""" diff --git a/src/test/test_cvss_helpers.py b/src/test/decision_points/test_cvss_helpers.py similarity index 89% rename from src/test/test_cvss_helpers.py rename to src/test/decision_points/test_cvss_helpers.py index bf9ea1e8..dc662dee 100644 --- a/src/test/test_cvss_helpers.py +++ b/src/test/decision_points/test_cvss_helpers.py @@ -20,7 +20,7 @@ import unittest import ssvc.decision_points.cvss.helpers as h -from ssvc.decision_points import SsvcDecisionPointValue +from ssvc.decision_points.base import DPV_REGISTRY, DP_REGISTRY, DecisionPointValue from ssvc.decision_points.cvss.base import CvssDecisionPoint @@ -33,17 +33,17 @@ def fake_ms_impacts() -> list[CvssDecisionPoint]: version="1.0.0", key=key, values=( - SsvcDecisionPointValue( + DecisionPointValue( name="None", key="N", description="No impact", ), - SsvcDecisionPointValue( + DecisionPointValue( name="Low", key="L", description="Low impact", ), - SsvcDecisionPointValue( + DecisionPointValue( name="High", key="H", description="High impact", @@ -54,8 +54,12 @@ def fake_ms_impacts() -> list[CvssDecisionPoint]: return dps -class MyTestCase(unittest.TestCase): +class TestCvssHelpers(unittest.TestCase): def setUp(self) -> None: + # reset the registry + for registry in DP_REGISTRY, DPV_REGISTRY: + registry.reset_registry() + self.dps = [] for i in range(3): dp = CvssDecisionPoint( @@ -64,12 +68,12 @@ def setUp(self) -> None: version="1.0", key=f"TDP{i}", values=( - SsvcDecisionPointValue( + DecisionPointValue( name=f"yes_{i}", description=f"yes_{i}", key=f"Y{i}", ), - SsvcDecisionPointValue( + DecisionPointValue( name=f"no_{i}", description=f"no_{i}", key=f"N{i}", diff --git a/src/test/decision_points/test_dp_base.py b/src/test/decision_points/test_dp_base.py new file mode 100644 index 00000000..9c340fc5 --- /dev/null +++ b/src/test/decision_points/test_dp_base.py @@ -0,0 +1,254 @@ +# Copyright (c) 2023-2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 + +import unittest + +import ssvc.decision_points.base as base +import ssvc.decision_points.ssvc.base + + +class MyTestCase(unittest.TestCase): + def setUp(self) -> None: + base.DP_REGISTRY.reset_registry() + base.DPV_REGISTRY.reset_registry() + + self.original_registry = base.REGISTERED_DECISION_POINTS.copy() + + # add multiple values + self.values = [] + for i in range(3): + self.values.append( + base.DecisionPointValue( + name=f"foo{i}", key=f"bar{i}", description=f"baz{i}" + ) + ) + + self.dp = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + name="foo", + key="bar", + description="baz", + version="1.0.0", + namespace="x_test", + values=tuple(self.values), + ) + + def tearDown(self) -> None: + # restore the original registry + base._reset_registered() + + def test_decision_point_basics(self): + from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned + + # inherits from mixins + mixins = [_Valued, _Base, _Keyed, _Versioned, _Namespaced] + for mixin in mixins: + self.assertIsInstance(self.dp, mixin) + + def test_registry(self): + # just by creating the objects, they should be registered + self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) + + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + name="asdfad", + key="asdfasdf", + description="asdfasdf", + version="1.33.1", + namespace="asdfasdf", + values=tuple(self.values), + ) + self.assertIn(dp2, base.REGISTERED_DECISION_POINTS) + + def test_registry_errors_on_duplicate_key(self): + # dp should already be registered from setUp + self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) + + # create a new decision point with the same key + with self.assertRaises(KeyError): + # the registry key is a combination of namespace, key, and version + + dp2 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, + key=self.dp.key, # same key as self.dp + version=self.dp.version, # same version as self.dp + values=tuple(self.values), + ) + + # should not be a problem if namespace, key or version are different + dp3 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace="x_test-extra", # different namespace + key=self.dp.key, # same key + version=self.dp.version, # same version + values=tuple(self.values), + ) + self.assertIn(dp3, base.REGISTERED_DECISION_POINTS) + # should not be a problem if key is different + dp4 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, # same namespace + key="different_key", # different key + version=self.dp.version, # same version + values=tuple(self.values), + ) + self.assertIn(dp4, base.REGISTERED_DECISION_POINTS) + # should not be a problem if version is different + dp5 = ssvc.decision_points.ssvc.base.DecisionPoint( + name="asdfad", + description="asdfasdf", + namespace=self.dp.namespace, # same namespace + key=self.dp.key, # same key + version="2.0.0", # different version + values=tuple(self.values), + ) + self.assertIn(dp5, base.REGISTERED_DECISION_POINTS) + + def test_registry(self): + # just by creating the objects, they should be registered + self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) + + dp2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint( + name="asdfad", + key="asdfasdf", + description="asdfasdf", + version="1.33.1", + namespace="x_test", + values=self.values, + ) + + dp2._comment = "asdfasdfasdf" + + self.assertIn(dp2, base.REGISTERED_DECISION_POINTS) + + def test_ssvc_value(self): + for i, obj in enumerate(self.values): + # should have name, key, description + self.assertEqual(obj.name, f"foo{i}") + self.assertEqual(obj.key, f"bar{i}") + self.assertEqual(obj.description, f"baz{i}") + + # should not have namespace, version + self.assertFalse(hasattr(obj, "namespace")) + self.assertFalse(hasattr(obj, "version")) + + def test_ssvc_decision_point(self): + obj = self.dp + # should have name, key, description, values, version, namespace + self.assertEqual(obj.name, "foo") + self.assertEqual(obj.key, "bar") + self.assertEqual(obj.description, "baz") + self.assertEqual(obj.version, "1.0.0") + self.assertEqual(obj.namespace, "x_test") + self.assertEqual(len(self.values), len(obj.values)) + + def test_ssvc_value_json_roundtrip(self): + for i, obj in enumerate(self.values): + json = obj.model_dump_json() + self.assertIsInstance(json, str) + self.assertGreater(len(json), 0) + + obj2 = base.DecisionPointValue.model_validate_json(json) + self.assertEqual(obj, obj2) + + def test_ssvc_decision_point_json_roundtrip(self): + obj = self.dp + + json = obj.model_dump_json() + self.assertIsInstance(json, str) + self.assertGreater(len(json), 0) + + obj2 = ssvc.decision_points.ssvc.base.SsvcDecisionPoint.model_validate_json( + json + ) + + # the objects should be equal + self.assertEqual(obj, obj2) + self.assertEqual(obj.model_dump(), obj2.model_dump()) + + def test_value_summaries_dict(self): + obj = self.dp + summaries = obj.value_summaries_dict + + # should be a dictionary + self.assertIsInstance(summaries, dict) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries dict should have str(ValueSummary) as the key + # and the ValueSummary as the value + for key, summary in summaries.items(): + # confirm the key is the string representation of the ValueSummary + self.assertEqual(key, str(summary)) + + # confirm the attributes of the ValueSummary + # key, version, and namespace come from the decision point + self.assertEqual(summary.key, obj.key) + self.assertEqual(summary.version, obj.version) + self.assertEqual(summary.namespace, obj.namespace) + # value comes from the list of values, and should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(summary.value, value_keys) + + def test_value_summaries_str(self): + obj = self.dp + summaries = obj.value_summaries_str + + # should be a list + self.assertIsInstance(summaries, list) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries list should have str(ValueSummary) as the key + for key in summaries: + # confirm the key is the string representation of the ValueSummary + self.assertIsInstance(key, str) + + # parse the key into its parts + (ns, k, v, val) = key.split(":") + # ns, k, v should come from the decision point + self.assertEqual(ns, obj.namespace) + self.assertEqual(k, obj.key) + self.assertEqual(v, obj.version) + # val should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(val, value_keys) + + def test_value_summaries(self): + obj = self.dp + summaries = obj.value_summaries + + # should be a list + self.assertIsInstance(summaries, list) + self.assertEqual(len(summaries), len(obj.values)) + + # the summaries list should be ValueSummary objects + for summary in summaries: + self.assertIsInstance(summary, base.ValueSummary) + # key, version, and namespace come from the decision point + self.assertEqual(summary.key, obj.key) + self.assertEqual(summary.version, obj.version) + self.assertEqual(summary.namespace, obj.namespace) + # value comes from the list of values, and should be a key to one of the values + value_keys = [v.key for v in obj.values] + self.assertIn(summary.value, value_keys) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/test/test_dp_helpers.py b/src/test/decision_points/test_dp_helpers.py similarity index 95% rename from src/test/test_dp_helpers.py rename to src/test/decision_points/test_dp_helpers.py index 03a6132d..9be7148d 100644 --- a/src/test/test_dp_helpers.py +++ b/src/test/decision_points/test_dp_helpers.py @@ -20,29 +20,30 @@ import unittest from copy import deepcopy -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.decision_points.helpers import dp_diff class MyTestCase(unittest.TestCase): def setUp(self) -> None: - self.dp1 = SsvcDecisionPoint( + self.dp1 = DecisionPoint( name="Test DP", key="test_dp", description="This is a test decision point", version="1.0.0", - values=[ - SsvcDecisionPointValue( + namespace='x_test', + values=( + DecisionPointValue( name="Yes", key="yes", description="Yes", ), - SsvcDecisionPointValue( + DecisionPointValue( name="No", key="no", description="No", ), - ], + ), ) self.dp2 = deepcopy(self.dp1) @@ -95,7 +96,7 @@ def test_major_version(self): # add one self.dp2.values = list(self.dp1.values) self.dp2.values.append( - SsvcDecisionPointValue( + DecisionPointValue( name="Maybe", key="maybe", description="Maybe", @@ -113,7 +114,7 @@ def test_minor_version_when_new_option_added(self): # add one self.dp2.values = list(self.dp1.values) self.dp2.values.append( - SsvcDecisionPointValue( + DecisionPointValue( name="Maybe", key="maybe", description="Maybe", diff --git a/src/test/dp_groups/__init__.py b/src/test/dp_groups/__init__.py new file mode 100644 index 00000000..bb9ae345 --- /dev/null +++ b/src/test/dp_groups/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.dp_groups. +""" diff --git a/src/test/dp_groups/test_dp_groups.py b/src/test/dp_groups/test_dp_groups.py new file mode 100644 index 00000000..7feefe94 --- /dev/null +++ b/src/test/dp_groups/test_dp_groups.py @@ -0,0 +1,165 @@ +# Copyright (c) 2023-2025 Carnegie Mellon University. +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact +# permission@sei.cmu.edu for full terms. +# [DISTRIBUTION STATEMENT A] This material has been approved for +# public release and unlimited distribution. Please see Copyright notice +# for non-US Government use and distribution. +# This Software includes and/or makes use of Third-Party Software each +# subject to its own license. +# DM24-0278 + +import unittest + +import ssvc.decision_points.ssvc.base +import ssvc.dp_groups.base as dpg +from ssvc.decision_points.base import DecisionPointValue + + +class MyTestCase(unittest.TestCase): + def setUp(self) -> None: + self.dps = [] + for i in range(10): + dp = ssvc.decision_points.ssvc.base.DecisionPoint( + name=f"Decision Point {i}", + key=f"DP_{i}", + namespace="x_test", + description=f"Description of Decision Point {i}", + version="1.0.0", + values=( + DecisionPointValue(name="foo", key="FOO", description="foo"), + DecisionPointValue(name="bar", key="BAR", description="bar"), + DecisionPointValue(name="baz", key="BAZ", description="baz"), + ), + ) + self.dps.append(dp) + + def tearDown(self) -> None: + pass + + def test_iter(self): + # add them to a decision point group + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + self.assertTrue(hasattr(g, "__iter__")) + + # iterate over the group + for dp in g: + self.assertIn(dp, self.dps) + + def test_len(self): + # add them to a decision point group + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + self.assertGreater(len(self.dps), 0) + self.assertEqual(len(self.dps), len(list(g.decision_points))) + self.assertEqual(len(self.dps), len(g)) + + def test_combo_strings(self): + # add them to a decision point group + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # get all the combinations + combos = list(g.combination_strings()) + + # assert that the number of combinations is the product of the number of values + # for each decision point + n_combos = 1 + for dp in self.dps: + n_combos *= len(dp.values) + self.assertEqual(n_combos, len(combos)) + + # assert that each combination is a tuple + for combo in combos: + self.assertEqual(len(self.dps), len(combo)) + self.assertIsInstance(combo, tuple) + # assert that each value in the combination is a string + for value in combo: + self.assertIsInstance(value, str) + # foo, bar, and baz should be in each combination to some degree + foo_count = sum(1 for v in combo if v.endswith("FOO")) + bar_count = sum(1 for v in combo if v.endswith("BAR")) + baz_count = sum(1 for v in combo if v.endswith("BAZ")) + for count in (foo_count, bar_count, baz_count): + # each count should be greater than or equal to 0 + self.assertGreaterEqual(count, 0) + # each count should be less than or equal to the length of the combination + self.assertLessEqual(count, len(combo)) + # the total count of foo, bar, and baz should be the same as the length of the combination + # indicating that no other values are present + total = sum((foo_count, bar_count, baz_count)) + self.assertEqual(len(combo), total) + + def test_json_roundtrip(self): + # add them to a decision point group + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # serialize the group to json + g_json = g.model_dump_json() + + # deserialize the json to a new group + g2 = dpg.DecisionPointGroup.model_validate_json(g_json) + # assert that the new group is the same as the old group + self.assertEqual(g_json, g2.model_dump_json()) + + def test_decision_points_dict(self): + # add them to a decision point group + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + + # get the decision points as a dictionary + dp_dict = g.decision_points_dict + + # assert that the dictionary is the correct length + self.assertEqual(len(self.dps), len(dp_dict)) + + # assert that each decision point is in the dictionary + for dp in self.dps: + self.assertIn(dp.str, dp_dict) + self.assertEqual(dp, dp_dict[dp.str]) + + def test_decision_points_str(self): + g = dpg.DecisionPointGroup( + name="Test Group", + description="Test Group", + decision_points=self.dps, + ) + dp_str = g.decision_points_str + self.assertEqual(len(self.dps), len(dp_str)) + + for i, dp in enumerate(self.dps): + self.assertIn(dp.str, dp_str) + # check that the string is the same as the decision point's string representation + # and they are in the same order + self.assertEqual(dp.str, dp_str[i]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/test/outcomes/__init__.py b/src/test/outcomes/__init__.py new file mode 100644 index 00000000..3c4a8bfe --- /dev/null +++ b/src/test/outcomes/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 Carnegie Mellon University and Contributors. +# - see Contributors.md for a full list of Contributors +# - see ContributionInstructions.md for information on how you can Contribute to this project +# Stakeholder Specific Vulnerability Categorization (SSVC) is +# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed +# with this Software or contact permission@sei.cmu.edu for full terms. +# Created, in part, with funding and support from the United States Government +# (see Acknowledgments file). This program may include and/or can make use of +# certain third party source code, object code, documentation and other files +# (“Third Party Software”). See LICENSE.md for more details. +# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the +# U.S. Patent and Trademark Office by Carnegie Mellon University +""" +Provides test classes for ssvc.outcomes. +""" diff --git a/src/test/test_outcomes.py b/src/test/outcomes/test_outcomes.py similarity index 87% rename from src/test/test_outcomes.py rename to src/test/outcomes/test_outcomes.py index 7c276c57..1a130481 100644 --- a/src/test/test_outcomes.py +++ b/src/test/outcomes/test_outcomes.py @@ -38,15 +38,20 @@ def test_outcome_group(self): values.append(OutcomeValue(key=x, name=x, description=x)) og = OutcomeGroup( - name="og", description="an outcome group", outcomes=tuple(values) + name="Outcome Group", + key="OG", + description="an outcome group", + namespace="x_test", + values=tuple(values), ) - self.assertEqual(og.name, "og") + self.assertEqual(og.name, "Outcome Group") + self.assertEqual(og.key, "OG") self.assertEqual(og.description, "an outcome group") self.assertEqual(len(og), len(ALPHABET)) - og_outcomes = list(og.outcomes) + og_outcomes = list(og.values) for i, letter in enumerate(ALPHABET): self.assertEqual(og_outcomes[i].key, letter) self.assertEqual(og_outcomes[i].name, letter) diff --git a/src/test/test_doc_helpers.py b/src/test/test_doc_helpers.py index 26b348a8..29899026 100644 --- a/src/test/test_doc_helpers.py +++ b/src/test/test_doc_helpers.py @@ -19,21 +19,21 @@ import unittest -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue from ssvc.doc_helpers import example_block, markdown_table class MyTestCase(unittest.TestCase): def setUp(self): - self.dp = SsvcDecisionPoint( + self.dp = DecisionPoint( namespace="x_test", name="test name", description="test description", key="TK", version="1.0.0", values=( - SsvcDecisionPointValue(name="A", key="A", description="A Definition"), - SsvcDecisionPointValue(name="B", key="B", description="B Definition"), + DecisionPointValue(name="A", key="A", description="A Definition"), + DecisionPointValue(name="B", key="B", description="B Definition"), ), ) diff --git a/src/test/test_doctools.py b/src/test/test_doctools.py index d5fd7d9b..5b38ca71 100644 --- a/src/test/test_doctools.py +++ b/src/test/test_doctools.py @@ -22,7 +22,7 @@ import tempfile import unittest -from ssvc.decision_points import SsvcDecisionPoint +from ssvc.decision_points.base import DecisionPoint from ssvc.doctools import ( EnsureDirExists, _filename_friendly, @@ -46,7 +46,7 @@ class MyTestCase(unittest.TestCase): def setUp(self) -> None: - self.dp = SsvcDecisionPoint.model_validate(_dp_dict) + self.dp = DecisionPoint.model_validate(_dp_dict) # create a temp working dir self.tempdir = tempfile.TemporaryDirectory() @@ -103,7 +103,11 @@ def test_dump_decision_point(self): self.assertIn("json", os.listdir(self.tempdir.name)) self.assertEqual(1, len(os.listdir(jsondir))) - file_created = os.listdir(jsondir)[0] + nsdir = os.path.join(jsondir, dp.namespace) + self.assertTrue(os.path.exists(nsdir)) + self.assertEqual(1, len(os.listdir(nsdir))) + + file_created = os.listdir(nsdir)[0] for word in dp.name.split(): self.assertIn(word.lower(), file_created) @@ -116,12 +120,14 @@ def test_dump_json(self): dp = self.dp jsondir = self.tempdir.name overwrite = False + nsdir = os.path.join(jsondir, dp.namespace) - _jsonfile = os.path.join(jsondir, f"{basename}.json") + _jsonfile = os.path.join(nsdir, f"{basename}.json") self.assertFalse(os.path.exists(_jsonfile)) # should create the file in the expected place json_file = dump_json(basename, dp, jsondir, overwrite) + self.assertEqual(_jsonfile, json_file) self.assertTrue(os.path.exists(json_file)) diff --git a/src/test/test_dp_base.py b/src/test/test_dp_base.py deleted file mode 100644 index ef057025..00000000 --- a/src/test/test_dp_base.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright (c) 2023-2025 Carnegie Mellon University. -# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE -# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. -# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, -# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT -# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR -# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE -# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE -# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM -# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. -# Licensed under a MIT (SEI)-style license, please see LICENSE or contact -# permission@sei.cmu.edu for full terms. -# [DISTRIBUTION STATEMENT A] This material has been approved for -# public release and unlimited distribution. Please see Copyright notice -# for non-US Government use and distribution. -# This Software includes and/or makes use of Third-Party Software each -# subject to its own license. -# DM24-0278 - -import unittest - -import ssvc.decision_points.base as base - - -class MyTestCase(unittest.TestCase): - def setUp(self) -> None: - self.original_registry = base.REGISTERED_DECISION_POINTS.copy() - - # add multiple values - self.values = [] - for i in range(3): - self.values.append( - base.SsvcDecisionPointValue( - name=f"foo{i}", key=f"bar{i}", description=f"baz{i}" - ) - ) - - self.dp = base.SsvcDecisionPoint( - name="foo", - key="bar", - description="baz", - version="1.0.0", - namespace="x_test", - values=tuple(self.values), - ) - - def tearDown(self) -> None: - # restore the original registry - base._reset_registered() - - def test_decision_point_basics(self): - from ssvc._mixins import _Base, _Keyed, _Namespaced, _Valued, _Versioned - - # inherits from mixins - mixins = [_Valued, _Base, _Keyed, _Versioned, _Namespaced] - for mixin in mixins: - self.assertIsInstance(self.dp, mixin) - - def test_registry(self): - # just by creating the objects, they should be registered - self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - - dp2 = base.SsvcDecisionPoint( - name="asdfad", - key="asdfasdf", - description="asdfasdf", - version="1.33.1", - namespace="asdfasdf", - values=tuple(self.values), - ) - - def test_registry(self): - # just by creating the objects, they should be registered - self.assertIn(self.dp, base.REGISTERED_DECISION_POINTS) - - dp2 = base.SsvcDecisionPoint( - name="asdfad", - key="asdfasdf", - description="asdfasdf", - version="1.33.1", - namespace="x_test", - values=self.values, - ) - - dp2._comment = "asdfasdfasdf" - - self.assertIn(dp2, base.REGISTERED_DECISION_POINTS) - - def test_ssvc_value(self): - for i, obj in enumerate(self.values): - # should have name, key, description - self.assertEqual(obj.name, f"foo{i}") - self.assertEqual(obj.key, f"bar{i}") - self.assertEqual(obj.description, f"baz{i}") - - # should not have namespace, version - self.assertFalse(hasattr(obj, "namespace")) - self.assertFalse(hasattr(obj, "version")) - - def test_ssvc_decision_point(self): - obj = self.dp - # should have name, key, description, values, version, namespace - self.assertEqual(obj.name, "foo") - self.assertEqual(obj.key, "bar") - self.assertEqual(obj.description, "baz") - self.assertEqual(obj.version, "1.0.0") - self.assertEqual(obj.namespace, "x_test") - self.assertEqual(len(self.values), len(obj.values)) - - def test_ssvc_value_json_roundtrip(self): - for i, obj in enumerate(self.values): - json = obj.model_dump_json() - self.assertIsInstance(json, str) - self.assertGreater(len(json), 0) - - obj2 = base.SsvcDecisionPointValue.model_validate_json(json) - self.assertEqual(obj, obj2) - - def test_ssvc_decision_point_json_roundtrip(self): - obj = self.dp - - json = obj.model_dump_json() - self.assertIsInstance(json, str) - self.assertGreater(len(json), 0) - - obj2 = base.SsvcDecisionPoint.model_validate_json(json) - - # the objects should be equal - self.assertEqual(obj, obj2) - self.assertEqual(obj.model_dump(), obj2.model_dump()) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/test_dp_groups.py b/src/test/test_dp_groups.py index 74adcd1b..e69de29b 100644 --- a/src/test/test_dp_groups.py +++ b/src/test/test_dp_groups.py @@ -1,96 +0,0 @@ -# Copyright (c) 2023-2025 Carnegie Mellon University. -# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE -# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. -# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, -# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT -# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR -# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE -# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE -# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM -# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. -# Licensed under a MIT (SEI)-style license, please see LICENSE or contact -# permission@sei.cmu.edu for full terms. -# [DISTRIBUTION STATEMENT A] This material has been approved for -# public release and unlimited distribution. Please see Copyright notice -# for non-US Government use and distribution. -# This Software includes and/or makes use of Third-Party Software each -# subject to its own license. -# DM24-0278 - -import unittest - -import ssvc.dp_groups.base as dpg -from ssvc.decision_points import SsvcDecisionPointValue - - -class MyTestCase(unittest.TestCase): - def setUp(self) -> None: - self.dps = [] - for i in range(10): - dp = dpg.SsvcDecisionPoint( - name=f"Decision Point {i}", - key=f"DP_{i}", - description=f"Description of Decision Point {i}", - version="1.0.0", - values=( - SsvcDecisionPointValue( - name="foo", key="FOO", description="foo" - ), - SsvcDecisionPointValue( - name="bar", key="BAR", description="bar" - ), - SsvcDecisionPointValue( - name="baz", key="BAZ", description="baz" - ), - ), - ) - self.dps.append(dp) - - def tearDown(self) -> None: - pass - - def test_iter(self): - # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( - name="Test Group", - description="Test Group", - decision_points=self.dps, - ) - - self.assertTrue(hasattr(g, "__iter__")) - - # iterate over the group - for dp in g: - self.assertIn(dp, self.dps) - - def test_len(self): - # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( - name="Test Group", - description="Test Group", - decision_points=self.dps, - ) - - self.assertGreater(len(self.dps), 0) - self.assertEqual(len(self.dps), len(list(g.decision_points))) - self.assertEqual(len(self.dps), len(g)) - - def test_json_roundtrip(self): - # add them to a decision point group - g = dpg.SsvcDecisionPointGroup( - name="Test Group", - description="Test Group", - decision_points=self.dps, - ) - - # serialize the group to json - g_json = g.model_dump_json() - - # deserialize the json to a new group - g2 = dpg.SsvcDecisionPointGroup.model_validate_json(g_json) - # assert that the new group is the same as the old group - self.assertEqual(g_json, g2.model_dump_json()) - - -if __name__ == "__main__": - unittest.main() diff --git a/src/test/test_policy_generator.py b/src/test/test_policy_generator.py index 901ab660..57f7b033 100644 --- a/src/test/test_policy_generator.py +++ b/src/test/test_policy_generator.py @@ -24,9 +24,8 @@ import networkx as nx import pandas as pd -from ssvc.decision_points import SsvcDecisionPoint, SsvcDecisionPointValue -from ssvc.dp_groups.base import SsvcDecisionPointGroup -from ssvc.outcomes.base import OutcomeGroup, OutcomeValue +from ssvc.decision_points.base import DecisionPoint, DecisionPointValue +from ssvc.dp_groups.base import DecisionPointGroup from ssvc.policy_generator import PolicyGenerator @@ -36,34 +35,43 @@ def setUp(self) -> None: self.dp_values = ["Yes", "No"] self.dp_names = ["Who", "What", "When", "Where"] - self.og = OutcomeGroup( + self.og = DecisionPoint( name="test", description="test", - outcomes=[ - OutcomeValue(key=c, name=c, description=c) - for c in self.og_names - ], + key="TEST", + namespace="x_test", + values=tuple( + [ + DecisionPointValue(key=c, name=c, description=c) + for c in self.og_names + ] + ), ) - self.dpg = SsvcDecisionPointGroup( + self.dpg = DecisionPointGroup( name="test", description="test", - decision_points=[ - SsvcDecisionPoint( - name=c, - description=c, - key=c, - values=[ - SsvcDecisionPointValue(name=v, key=v, description=v) - for v in self.dp_values - ], - ) - for c in self.dp_names - ], + decision_points=tuple( + [ + DecisionPoint( + name=c, + description=c, + key=c, + namespace="x_test", + values=tuple( + [ + DecisionPointValue(name=v, key=v, description=v) + for v in self.dp_values + ] + ), + ) + for c in self.dp_names + ] + ), ) def test_pg_init(self): self.assertEqual(4, len(self.dpg.decision_points)) - self.assertEqual(4, len(self.og.outcomes)) + self.assertEqual(4, len(self.og.values)) pg = PolicyGenerator(dp_group=self.dpg, outcomes=self.og) for w in pg.outcome_weights: @@ -240,8 +248,8 @@ def test_emit_policy(self): for dpg in pg.dpg.decision_points: self.assertIn(dpg.name, stdout) - for og in pg.outcomes.outcomes: - self.assertIn(og.name.lower(), stdout) + for og in pg.outcomes.values: + self.assertIn(og.name, stdout) def test_create_policy(self): pg = PolicyGenerator( @@ -263,15 +271,19 @@ def test_create_policy(self): self.assertIsInstance(pg.policy, pd.DataFrame) self.assertEqual(16, len(pg.policy)) + idx_cols = [col for col in pg.policy.columns if col.startswith("idx_")] + other_cols = [col for col in pg.policy.columns if not col.startswith("idx_")] + for c in self.dp_names: - self.assertIn(c, pg.policy.columns) - self.assertIn(f"idx_{c}", pg.policy.columns) + + self.assertTrue(any([c in col for col in other_cols])) + self.assertTrue(any([c in col for col in idx_cols])) self.assertIn("outcome", pg.policy.columns) self.assertIn("idx_outcome", pg.policy.columns) for outcome in self.og_names: - self.assertIn(outcome, pg.policy.outcome.values) + self.assertTrue(any([outcome in val for val in pg.policy.outcome.values])) def test_validate_paths(self): pg = PolicyGenerator( @@ -324,12 +336,8 @@ def test_confirm_topological_order(self): self.assertIsNone(pg._confirm_topological_order([0, 1, 2, 3, 4, 5])) self.assertIsNone(pg._confirm_topological_order([0, 1, 3, 2, 4, 5])) - self.assertRaises( - ValueError, pg._confirm_topological_order, [0, 1, 2, 4, 3, 5] - ) - self.assertRaises( - ValueError, pg._confirm_topological_order, [0, 1, 2, 3, 5] - ) + self.assertRaises(ValueError, pg._confirm_topological_order, [0, 1, 2, 4, 3, 5]) + self.assertRaises(ValueError, pg._confirm_topological_order, [0, 1, 2, 3, 5]) if __name__ == "__main__": diff --git a/src/test/test_schema.py b/src/test/test_schema.py index 3110d5ca..383f60d4 100644 --- a/src/test/test_schema.py +++ b/src/test/test_schema.py @@ -30,9 +30,9 @@ from ssvc.decision_points.base import REGISTERED_DECISION_POINTS # importing these causes the decision points to register themselves -from ssvc.decision_points.critical_software import CRITICAL_SOFTWARE_1 # noqa -from ssvc.decision_points.high_value_asset import HIGH_VALUE_ASSET_1 # noqa -from ssvc.decision_points.in_kev import IN_KEV_1 +from ssvc.decision_points.ssvc.critical_software import CRITICAL_SOFTWARE_1 # noqa +from ssvc.decision_points.ssvc.high_value_asset import HIGH_VALUE_ASSET_1 # noqa +from ssvc.decision_points.ssvc.in_kev import IN_KEV_1 from ssvc.dp_groups.cvss.collections import ( CVSSv1, CVSSv2, @@ -100,15 +100,13 @@ def test_decision_point_validation(self): loaded = json.loads(as_json) try: - Draft202012Validator( - {"$ref": schema_url}, registry=registry - ).validate(loaded) + Draft202012Validator({"$ref": schema_url}, registry=registry).validate( + loaded + ) except jsonschema.exceptions.ValidationError as e: exp = e - self.assertIsNone( - exp, f"Validation failed for {dp.name} {dp.version}" - ) + self.assertIsNone(exp, f"Validation failed for {dp.name} {dp.version}") self.logger.debug( f"Validation passed for Decision Point ({dp.namespace}) {dp.name} v{dp.version}" ) @@ -121,15 +119,13 @@ def test_decision_point_group_validation(self): loaded = json.loads(as_json) try: - Draft202012Validator( - {"$ref": schema_url}, registry=registry - ).validate(loaded) + Draft202012Validator({"$ref": schema_url}, registry=registry).validate( + loaded + ) except jsonschema.exceptions.ValidationError as e: exp = e - self.assertIsNone( - exp, f"Validation failed for {dpg.name} {dpg.version}" - ) + self.assertIsNone(exp, f"Validation failed for {dpg.name} {dpg.version}") self.logger.debug( f"Validation passed for Decision Point Group {dpg.name} v{dpg.version}" )