diff --git a/README.md b/README.md index eb9ab2de2..415cda4b3 100644 --- a/README.md +++ b/README.md @@ -106,38 +106,35 @@ A collection of demos utilizing trestle can be found in the related project [com Compliance trestle is currently stable and is based on NIST OSCAL version 1.0.4, with active development continuing. -## Community call +## Community meetings and communications -We would like to share development in progress for compliance trestle, coming soon and get feedback from community on what features would they like to see in compliance trestle.\ -The community call will happen every 2 week(s) on Tuesday at 10.00am EST.\ -Meeting information: +##### Scheduled meetings -``` -Compliance Trestle Community Call - -Join from the meeting link -https://ibm.webex.com/ibm/j.php?MTID=m030fdef5ac2d09f46f04813bb5e9dc6b -Tuesday, September 5, 2023 10:00 AM | 30 minutes | (UTC-04:00) Eastern Time (US & Canada) -Occurs every 2 week(s) on Tuesday effective 9/5/2023 from 10:00 AM to 10:30 AM, (UTC-04:00) Eastern Time (US & Canada) - -Join by meeting number -Meeting number (access code): 146 967 4515 - -Tap to join from a mobile device (attendees only) -1-844-531-0958,,1469674515#43533276# United States Toll Free -+1-669-234-1178,,1469674515#43533276# United States Toll -Some mobile devices may ask attendees to enter a numeric password. - -Join by phone -1-844-531-0958 United States Toll Free -1-669-234-1178 United States Toll -Global call-in numbers | Toll-free calling restrictions - -Join from a video system or application -Dial 1469674515@ibm.webex.com -You can also dial 173.243.2.68 and enter your meeting number. +Please attend! All are invited. -``` +**When**: Every other Tuesday at 10:00 ET [convert to your local time](https://dateful.com/convert/est-edt-eastern-time) + +To discover the actual meeting dates: + +- Go to [Google Calendar](https://calendar.google.com/calendar/u/0/embed?src=0b8u5el8ta4s93t2cm72tuvhhk@group.calendar.google.com&ctz=America/Los_Angeles) +- Look at entries in `Tue` day of week for *Compliance Trestle Community Call* +- To add to your calendar, `click` on `Compliance Trestle Community Call` and choose `copy to my calendar` + +**Where**: [https://zoom.us/j/92729235315](https://zoom.us/j/92729235315) + +- Meeting Id: 927 2923 5315 + +- Passcode: 233140 + +- **Note**: Use the passcode above to login to Zoom (or you can login to Zoom using another account like Google, Facebook) + +**What**: Meeting agenda and notes [Google Docs](https://docs.google.com/document/d/1z9xvt-Z97j4CtEH1-nR9sMWul7jQkUi_fNY7BdMPgxM/edit#heading=h.nohkp1kbeduj) + +##### Chat anytime + +Slack: [# compliance-grc](https://cloud-native.slack.com/archives/C066TMUBEL8) + +- **Note**: You can login to Slack using another account like Google, Apple ## Contributing to Trestle diff --git a/setup.cfg b/setup.cfg index 9118fa3f6..72d7a001e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,7 +28,7 @@ include_package_data = True install_requires = attrs ilcli - cryptography==41.0.6 + cryptography==42.0.0 paramiko==3.4.0 ruamel.yaml furl diff --git a/tests/data/csv/bp.sample.v4.csv b/tests/data/csv/bp.sample.v4.csv new file mode 100644 index 000000000..ec11adf54 --- /dev/null +++ b/tests/data/csv/bp.sample.v4.csv @@ -0,0 +1,12 @@ +Reference_Id,Rule_Id,Rule_Description,Check_Id,Check_Description,Fetcher,Fetcher_Description,Profile_Source,Profile_Description,Component_Type,Control_Id_List,Component_Title,Component_Description,Parameter_Id,Parameter_Description,Parameter_Value_Default,Parameter_Value_Alternatives,Parameter_Id2,Parameter_Description2,Parameter_Value_Default2,Parameter_Value_Alternatives2,Namespace +column description,column description,column description,,,,,,,,,,,,,,,,,,, +3000020,account_owner_authorized_ip_range_configured,Ensure authorized IP ranges are configured by the account owner,account_owner_authorized_ip_range_configured,Check whether authorized IP ranges are configured by the account owner,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,sc-7_smt.a sc-7_smt.b sc-7.3 sc-7.4_smt.a sc-7.5 ia-3,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000021,iam_admin_role_users_per_account_maxcount,Ensure there are no more than # IAM administrators configured per account,iam_admin_role_users_per_account_maxcount,Check whether there are no more than # IAM administrators configured per account,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-6 ac-5_smt.c,IAM,IAM,allowed_admins_per_account,Maximum allowed administrators per,10,10,allowed_admins_per_account2,Maximum allowed administrators per2,20,20,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000022,iam_cos_public_access_disabled,Ensure Cloud Object Storage public access is disabled in IAM settings (not applicable to ACLs managed using S3 APIs),iam_cos_public_access_disabled,Check whether Cloud Object Storage public access is disabled in IAM settings (not applicable to ACLs managed using S3 APIs),,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-3 ac-4 ac-6 sc-7_smt.a sc-7_smt.b sc-7.4_smt.a ac-14_smt.a cm-7_smt.a cm-7_smt.b,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000023,iam_account_owner_no_api_key,Ensure the account owner does not have an IBM Cloud API key created in IAM,iam_account_owner_no_api_key,Check whether the account owner does not have an IBM Cloud API key created in IAM,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-2_smt.d ac-3 ac-5_smt.c ac-6,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000024,iam_api_keys_rotation_configured,Ensure IBM Cloud API keys that are managed in IAM are rotated at least every # days,iam_api_keys_rotation_configured,Check whether IBM Cloud API keys that are managed in IAM are rotated at least every # days,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ia-5_smt.g,IAM,IAM,api_keys_rotated_days,API Keys Rotated,"x, y, z",,api_keys_rotated_days2,API Keys Rotated2,"r, s, t",,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000027,iam_account_owner_api_key_restrictions_configured,Ensure permissions for API key creation are limited and configured in IAM settings for the account owner,iam_account_owner_api_key_restrictions_configured,Check whether permissions for API key creation are limited and configured in IAM settings for the account owner,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-2_smt.d ac-3 ac-5_smt.c ac-6,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000029,iam_admin_role__user_maxcount,Ensure IAM-enabled services have no more than # users with the IAM administrator role,iam_admin_role__user_maxcount,Check whether IAM-enabled services have no more than # users with the IAM administrator role,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-6 ac-5_smt.c ia-7,IAM,IAM,no_of_admins_for_iam,Maximum no of IAM user,"a, b, c",,no_of_admins_for_iam2,Maximum no of IAM user2,"d, e, f",,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000030,iam_serviceID_policies_attached_to_access_groups_or_roles,Ensure IAM policies for service IDs are attached only to groups or roles,iam_serviceID_policies_attached_to_access_groups_or_roles,Check whether IAM policies for service IDs are attached only to groups or roles,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-3 ac-6 ac-2_smt.d ac-5_smt.c ia-7,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000031,iam_logDNA_enabled,Ensure Identity and Access Management (IAM) is enabled with audit logging,iam_logDNA_enabled,Check whether Identity and Access Management (IAM) is enabled with audit logging,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,au-2_smt.a au-2_smt.d si-4_smt.a si-4_smt.b si-4_smt.c au-12_smt.a au-12_smt.b au-12_smt.c au-3 au-8_smt.a au-8_smt.b au-8.1_smt.a au-8.1_smt.b ca-7_smt.d,IAM,IAM,,,,,,,,,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd +3000032,iam_admin_role_serviceid_maxcount,Ensure IAM-enabled services have no more than # service IDs with the IAM administrator role,iam_admin_role_serviceid_maxcount,Check whether IAM-enabled services have no more than # service IDs with the IAM administrator role,,,https://github.com/usnistgov/oscal-content/blob/main/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_HIGH-baseline_profile.json,NIST Special Publication 800-53 Revision 5 HIGH IMPACT BASELINE,Service,ac-6 ac-5_smt.c ia-7,IAM,IAM,no_of_service_id_admins_for_iam,Maximum no of IAM Service ID,"3, 4, 5",,no_of_service_id_admins_for_iam2,Maximum no of IAM Service ID2,"10, 11, 12",,http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd diff --git a/tests/data/json/comp_def_more_params.json b/tests/data/json/comp_def_more_params.json new file mode 100644 index 000000000..7918febd4 --- /dev/null +++ b/tests/data/json/comp_def_more_params.json @@ -0,0 +1,503 @@ +{ + "component-definition": { + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a007209f", + "metadata": { + "title": "comp def a", + "last-modified": "2021-07-19T14:03:03+00:00", + "version": "0.21.0", + "oscal-version": "1.0.2", + "roles": [ + { + "id": "prepared-by", + "title": "Indicates the organization that created this content." + }, + { + "id": "prepared-for", + "title": "Indicates the organization for which this content was created.." + }, + { + "id": "content-approver", + "title": "Indicates the organization responsible for all content represented in the \"document\"." + } + ], + "parties": [ + { + "uuid": "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2", + "type": "organization", + "name": "ACME", + "remarks": "ACME company" + }, + { + "uuid": "481856b6-16e4-4993-a3ed-2fb242ce235b", + "type": "organization", + "name": "Customer", + "remarks": "Customer for the Component Definition" + }, + { + "uuid": "2dc8b17f-daca-44a1-8a1d-c290120ea5e2", + "type": "organization", + "name": "ISV", + "remarks": "ISV for the Component Definition" + } + ], + "responsible-parties": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ] + }, + "components": [ + { + "uuid": "8220b305-0271-45f9-8a21-40ab6f197f70", + "type": "Service", + "title": "comp_ca", + "description": "comp ca", + "props": [ + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "top_shared_rule_1", + "class": "Rule_Id", + "remarks": "rule_1" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "top shared rule 1 in aa", + "remarks": "rule_1" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_x", + "class": "Parameter_Id", + "remarks": "rule_x" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param x in aa", + "class": "Parameter_Description", + "remarks": "rule_x" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_x_aa_opt_1\", \"shared_param_x_aa_opt_2\", \"shared_param_x_aa_opt_3\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_x" + } + ], + "control-implementations": [ + { + "uuid": "76e89b67-3d6b-463d-90df-ec56a46c6069", + "source": "trestle://profiles/comp_prof_aa/profile.json", + "description": "trestle comp prof aa", + "props": [ + { + "name": "profile_name", + "ns": "https://trestle/prof_ns", + "value": "trestle prof aa", + "class": "trestle_profile_name" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_aa_1", + "class": "Rule_Id", + "remarks": "rule_2" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule aa 1", + "remarks": "rule_2" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_aa_2", + "class": "Rule_Id", + "remarks": "rule_3" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule aa 2", + "class": "Rule_Description", + "remarks": "rule_3" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_for_ca_1", + "class": "Parameter_Id", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param 1 in aa", + "class": "Parameter_Description", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_for_ca_1_ca_opt_1\", \"shared_param_for_ca_1_ca_opt_2\", \"shared_param_for_ca_1_ca_opt_3\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_1" + }, + { + "name": "Parameter_Id2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "allowed_admins_per_account2", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Maximum allowed administrators per2", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Default2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "20", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "[\"20\", \"30\"]", + "remarks": "rule_1" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_ca_opt_1" + ], + "remarks": "set shared param aa 3" + }, + { + "param-id": "ac-1_prm_3", + "values": [ + "set by comp aa ci" + ] + } + ], + "implemented-requirements": [ + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7500", + "control-id": "ac-1", + "description": "imp req prose for ac-1 from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_ca_opt_1" + ], + "remarks": "set shared param aa 1" + }, + { + "param-id": "ac-1_prm_3", + "values": [ + "set by comp aa imp req" + ] + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "ac-1_smt.a", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072200", + "description": "statement prose for part a. from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "comp_rule_aa_1" + }, + { + "name": "implementation-status", + "value": "partial" + } + ] + } + ] + }, + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7599", + "control-id": "ac-3", + "description": "imp req prose for ac-3 from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ] + } + ] + } + ] + }, + { + "uuid": "8220b305-0271-45f9-8a21-40ab6f197f71", + "type": "Service", + "title": "comp_cc", + "description": "comp cc", + "props": [ + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "top_shared_rule_1", + "class": "Rule_Id", + "remarks": "rule_1" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "top shared rule 1 in ab", + "remarks": "rule_1" + } + ], + "control-implementations": [ + { + "uuid": "76e89b67-3d6b-463d-90df-ec56a46c6069", + "source": "trestle://profiles/comp_prof_ab/profile.json", + "description": "trestle comp prof ab", + "props": [ + { + "name": "profile_name", + "ns": "https://trestle/prof_ns", + "value": "trestle prof ab", + "class": "trestle_profile_name" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_ab_1", + "class": "Rule_Id", + "remarks": "rule_2" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule ab 1", + "remarks": "rule_2" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_ab_5", + "class": "Rule_Id", + "remarks": "rule_5" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule ab 5", + "class": "Rule_Description", + "remarks": "rule_5" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_for_ca_1", + "class": "Parameter_Id", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param 1 in ab", + "class": "Parameter_Description", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_for_ca_1_cc_opt_1\", \"shared_param_for_ca_1_cc_opt_2\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_1" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_cc_opt_1" + ], + "remarks": "set param ab 1" + } + ], + "implemented-requirements": [ + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7501", + "control-id": "ac-1", + "description": "", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "ac-1_smt.a", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072201", + "description": "", + "props": [ + { + "name": "Rule_Id", + "value": "comp_rule_ab_1" + }, + { + "name": "implementation-status", + "value": "partial" + } + ] + }, + { + "statement-id": "ac-1_smt.b", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072209", + "description": "" + } + ] + }, + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7502", + "control-id": "at-1", + "description": "imp req prose for at-1 from comp ab", + "props": [ + { + "name": "Rule_Id", + "value": "rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "at-1_smt.b", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072202", + "description": "statement prose for part b. from comp ab", + "props": [ + { + "name": "Rule_Id", + "value": "rule_5" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ] + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/data/json/comp_def_more_params_dup.json b/tests/data/json/comp_def_more_params_dup.json new file mode 100644 index 000000000..7918febd4 --- /dev/null +++ b/tests/data/json/comp_def_more_params_dup.json @@ -0,0 +1,503 @@ +{ + "component-definition": { + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a007209f", + "metadata": { + "title": "comp def a", + "last-modified": "2021-07-19T14:03:03+00:00", + "version": "0.21.0", + "oscal-version": "1.0.2", + "roles": [ + { + "id": "prepared-by", + "title": "Indicates the organization that created this content." + }, + { + "id": "prepared-for", + "title": "Indicates the organization for which this content was created.." + }, + { + "id": "content-approver", + "title": "Indicates the organization responsible for all content represented in the \"document\"." + } + ], + "parties": [ + { + "uuid": "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2", + "type": "organization", + "name": "ACME", + "remarks": "ACME company" + }, + { + "uuid": "481856b6-16e4-4993-a3ed-2fb242ce235b", + "type": "organization", + "name": "Customer", + "remarks": "Customer for the Component Definition" + }, + { + "uuid": "2dc8b17f-daca-44a1-8a1d-c290120ea5e2", + "type": "organization", + "name": "ISV", + "remarks": "ISV for the Component Definition" + } + ], + "responsible-parties": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ] + }, + "components": [ + { + "uuid": "8220b305-0271-45f9-8a21-40ab6f197f70", + "type": "Service", + "title": "comp_ca", + "description": "comp ca", + "props": [ + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "top_shared_rule_1", + "class": "Rule_Id", + "remarks": "rule_1" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "top shared rule 1 in aa", + "remarks": "rule_1" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_x", + "class": "Parameter_Id", + "remarks": "rule_x" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param x in aa", + "class": "Parameter_Description", + "remarks": "rule_x" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_x_aa_opt_1\", \"shared_param_x_aa_opt_2\", \"shared_param_x_aa_opt_3\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_x" + } + ], + "control-implementations": [ + { + "uuid": "76e89b67-3d6b-463d-90df-ec56a46c6069", + "source": "trestle://profiles/comp_prof_aa/profile.json", + "description": "trestle comp prof aa", + "props": [ + { + "name": "profile_name", + "ns": "https://trestle/prof_ns", + "value": "trestle prof aa", + "class": "trestle_profile_name" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_aa_1", + "class": "Rule_Id", + "remarks": "rule_2" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule aa 1", + "remarks": "rule_2" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_aa_2", + "class": "Rule_Id", + "remarks": "rule_3" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule aa 2", + "class": "Rule_Description", + "remarks": "rule_3" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_for_ca_1", + "class": "Parameter_Id", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param 1 in aa", + "class": "Parameter_Description", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_for_ca_1_ca_opt_1\", \"shared_param_for_ca_1_ca_opt_2\", \"shared_param_for_ca_1_ca_opt_3\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_1" + }, + { + "name": "Parameter_Id2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "allowed_admins_per_account2", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Maximum allowed administrators per2", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Default2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "20", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives2", + "ns": "http://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "[\"20\", \"30\"]", + "remarks": "rule_1" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_ca_opt_1" + ], + "remarks": "set shared param aa 3" + }, + { + "param-id": "ac-1_prm_3", + "values": [ + "set by comp aa ci" + ] + } + ], + "implemented-requirements": [ + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7500", + "control-id": "ac-1", + "description": "imp req prose for ac-1 from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_ca_opt_1" + ], + "remarks": "set shared param aa 1" + }, + { + "param-id": "ac-1_prm_3", + "values": [ + "set by comp aa imp req" + ] + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "ac-1_smt.a", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072200", + "description": "statement prose for part a. from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "comp_rule_aa_1" + }, + { + "name": "implementation-status", + "value": "partial" + } + ] + } + ] + }, + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7599", + "control-id": "ac-3", + "description": "imp req prose for ac-3 from comp aa", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ] + } + ] + } + ] + }, + { + "uuid": "8220b305-0271-45f9-8a21-40ab6f197f71", + "type": "Service", + "title": "comp_cc", + "description": "comp cc", + "props": [ + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "top_shared_rule_1", + "class": "Rule_Id", + "remarks": "rule_1" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "top shared rule 1 in ab", + "remarks": "rule_1" + } + ], + "control-implementations": [ + { + "uuid": "76e89b67-3d6b-463d-90df-ec56a46c6069", + "source": "trestle://profiles/comp_prof_ab/profile.json", + "description": "trestle comp prof ab", + "props": [ + { + "name": "profile_name", + "ns": "https://trestle/prof_ns", + "value": "trestle prof ab", + "class": "trestle_profile_name" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_ab_1", + "class": "Rule_Id", + "remarks": "rule_2" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule ab 1", + "remarks": "rule_2" + }, + { + "name": "Rule_Id", + "ns": "http://comp_ns", + "value": "comp_rule_ab_5", + "class": "Rule_Id", + "remarks": "rule_5" + }, + { + "name": "Rule_Description", + "ns": "http://comp_ns", + "value": "comp rule ab 5", + "class": "Rule_Description", + "remarks": "rule_5" + }, + { + "name": "Parameter_Id", + "ns": "http://comp_ns", + "value": "shared_param_for_ca_1", + "class": "Parameter_Id", + "remarks": "rule_1" + }, + { + "name": "Parameter_Description", + "ns": "http://comp_ns", + "value": "shared param 1 in ab", + "class": "Parameter_Description", + "remarks": "rule_1" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "http://comp_ns", + "value": "[\"shared_param_for_ca_1_cc_opt_1\", \"shared_param_for_ca_1_cc_opt_2\"]", + "class": "Parameter_Value_Alternatives", + "remarks": "rule_1" + } + ], + "set-parameters": [ + { + "param-id": "shared_param_for_ca_1", + "values": [ + "shared_param_for_ca_1_cc_opt_1" + ], + "remarks": "set param ab 1" + } + ], + "implemented-requirements": [ + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7501", + "control-id": "ac-1", + "description": "", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "ac-1_smt.a", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072201", + "description": "", + "props": [ + { + "name": "Rule_Id", + "value": "comp_rule_ab_1" + }, + { + "name": "implementation-status", + "value": "partial" + } + ] + }, + { + "statement-id": "ac-1_smt.b", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072209", + "description": "" + } + ] + }, + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7502", + "control-id": "at-1", + "description": "imp req prose for at-1 from comp ab", + "props": [ + { + "name": "Rule_Id", + "value": "rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ], + "statements": [ + { + "statement-id": "at-1_smt.b", + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a0072202", + "description": "statement prose for part b. from comp ab", + "props": [ + { + "name": "Rule_Id", + "value": "rule_5" + }, + { + "name": "implementation-status", + "value": "implemented" + } + ] + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/data/json/full_profile_rev5.json b/tests/data/json/full_profile_rev5.json index 233ece5f7..895fffcda 100644 --- a/tests/data/json/full_profile_rev5.json +++ b/tests/data/json/full_profile_rev5.json @@ -99,6 +99,8 @@ "include-controls": [ { "with-ids": [ + "ac-1", + "ac-2.3", "sc-21" ] } diff --git a/tests/data/json/test_compdef_rev5.json b/tests/data/json/test_compdef_rev5.json new file mode 100644 index 000000000..e0c5cb61f --- /dev/null +++ b/tests/data/json/test_compdef_rev5.json @@ -0,0 +1,127 @@ +{ + "component-definition": { + "uuid": "2652b814-2a6b-4b6d-a0ae-8bc7a007209f", + "metadata": { + "title": "example FedRAMP component definition", + "last-modified": "2021-07-19T14:03:03+00:00", + "version": "0.21.0", + "oscal-version": "1.0.2", + "roles": [ + { + "id": "prepared-by", + "title": "Indicates the organization that created this content." + }, + { + "id": "prepared-for", + "title": "Indicates the organization for which this content was created.." + }, + { + "id": "content-approver", + "title": "Indicates the organization responsible for all content represented in the \"document\"." + } + ], + "parties": [ + { + "uuid": "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2", + "type": "organization", + "name": "ACME", + "remarks": "ACME company" + }, + { + "uuid": "481856b6-16e4-4993-a3ed-2fb242ce235b", + "type": "organization", + "name": "Customer", + "remarks": "Customer for the Component Definition" + }, + { + "uuid": "2dc8b17f-daca-44a1-8a1d-c290120ea5e2", + "type": "organization", + "name": "ISV", + "remarks": "ISV for the Component Definition" + } + ], + "responsible-parties": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ] + }, + "components": [ + { + "uuid": "8220b305-0271-45f9-8a21-40ab6f197f75", + "type": "Service", + "title": "test_component", + "description": "This is a test component", + "control-implementations": [ + { + "uuid": "76e89b67-3d6b-463d-90df-ec56a46c6069", + "source": "trestle://profiles/full_profile_rev5/profile.json", + "description": "trestle comp rev5", + "implemented-requirements": [ + { + "uuid": "ca5ea4c5-ba51-4b1d-932a-5606891b7600", + "control-id": "sc-21", + "description": "imp req prose for sc-21 from test component", + "props": [ + { + "name": "Rule_Id", + "value": "top_shared_rule_1" + }, + { + "name": "implementation-status", + "value": "implemented" + }, + { + "name": "control-origination", + "value": "system-specific" + }, + { + "name": "control-origination", + "value": "customer-configured" + } + ], + "responsible-roles": [ + { + "role-id": "prepared-by", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + }, + { + "role-id": "prepared-for", + "party-uuids": [ + "481856b6-16e4-4993-a3ed-2fb242ce235b", + "2dc8b17f-daca-44a1-8a1d-c290120ea5e2" + ] + }, + { + "role-id": "content-approver", + "party-uuids": [ + "ce1f379a-fcdd-485a-a7b7-6f02c0763dd2" + ] + } + ] + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/test_utils.py b/tests/test_utils.py index 50244db2f..354d17a97 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -369,9 +369,8 @@ def setup_for_component_definition(tmp_trestle_dir: pathlib.Path, monkeypatch: M return comp_name -def setup_component_generate(tmp_trestle_dir: pathlib.Path) -> str: +def setup_component_generate(tmp_trestle_dir: pathlib.Path, comp_name='comp_def_a') -> str: """Create the compdef, profile and catalog content component-generate.""" - comp_name = 'comp_def_a' load_from_json(tmp_trestle_dir, comp_name, comp_name, comp.ComponentDefinition) for prof_name in 'comp_prof,comp_prof_aa,comp_prof_ab,comp_prof_ba,comp_prof_bb'.split(','): load_from_json(tmp_trestle_dir, prof_name, prof_name, prof.Profile) @@ -461,10 +460,10 @@ def setup_for_ssp( prof_name: str, output_name: str, use_yaml: bool = False, - leveraged_ssp_name: str = '' + leveraged_ssp_name: str = '', + comp_names='comp_def_a,comp_def_b' ) -> Tuple[argparse.Namespace, pathlib.Path]: """Create the comp_def, profile and catalog content needed for ssp-generate.""" - comp_names = 'comp_def_a,comp_def_b' for comp_name in comp_names.split(','): load_from_json(tmp_trestle_dir, comp_name, comp_name, comp.ComponentDefinition) prof_name_list = [prof_name] @@ -493,6 +492,32 @@ def setup_for_ssp( return args, yaml_path +def setup_for_ssp_fedramp( + tmp_trestle_dir: pathlib.Path, + output_name: str, +) -> argparse.Namespace: + """Load profile and component needed for ssp-generate with FedRAMP profile.""" + prof_name = 'full_profile_rev5' + comp_name = 'test_compdef_rev5' + load_from_json(tmp_trestle_dir, prof_name, prof_name, prof.Profile) + load_from_json(tmp_trestle_dir, comp_name, comp_name, comp.ComponentDefinition) + + args = argparse.Namespace( + trestle_root=tmp_trestle_dir, + profile=prof_name, + compdefs=comp_name, + leveraged_ssp=None, + output=output_name, + verbose=0, + overwrite_header_values=False, + yaml_header=None, + allowed_sections=None, + force_overwrite=None + ) + + return args + + def make_file_hidden(file_path: pathlib.Path, if_dot=False) -> None: """ Make a file hidden on windows. diff --git a/tests/trestle/core/commands/author/component_test.py b/tests/trestle/core/commands/author/component_test.py index 2421ace3d..508b14f71 100644 --- a/tests/trestle/core/commands/author/component_test.py +++ b/tests/trestle/core/commands/author/component_test.py @@ -187,3 +187,18 @@ def test_component_generate_missing_control(tmp_trestle_dir: pathlib.Path, monke test_utils.execute_command_and_assert(generate_cmd, 0, monkeypatch) _, err = capsys.readouterr() assert "Component comp_aa references controls {'ac-1'} not in profile." in err + + +def test_component_generate_more_than_one_param(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: + """Test component generate with more than 1 parameters per rule.""" + comp_name = test_utils.setup_component_generate(tmp_trestle_dir, 'comp_def_more_params') + + generate_cmd = f'trestle author component-generate -n {comp_name} -o {md_path}' + + # generate the md first time + test_utils.execute_command_and_assert(generate_cmd, CmdReturnCodes.SUCCESS.value, monkeypatch) + + assem_name = 'assem_comp' + # now assemble component generated + assemble_cmd = f'trestle author component-assemble -m {md_path} -n {comp_name} -o {assem_name}' + test_utils.execute_command_and_assert(assemble_cmd, CmdReturnCodes.SUCCESS.value, monkeypatch) diff --git a/tests/trestle/core/commands/author/ssp_test.py b/tests/trestle/core/commands/author/ssp_test.py index 6f2646a14..8f9eaf550 100644 --- a/tests/trestle/core/commands/author/ssp_test.py +++ b/tests/trestle/core/commands/author/ssp_test.py @@ -19,7 +19,7 @@ from _pytest.monkeypatch import MonkeyPatch from tests import test_utils -from tests.test_utils import FileChecker, setup_for_ssp +from tests.test_utils import FileChecker, setup_for_ssp, setup_for_ssp_fedramp import trestle.core.generators as gens import trestle.core.generic_oscal as generic @@ -485,6 +485,17 @@ def test_ssp_assemble(tmp_trestle_dir: pathlib.Path) -> None: assert orig_ssp_path.stat().st_mtime > orig_file_creation +def test_ssp_assemble_fedramp_profile(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: + """Tests ssp assemble with a fedramp profile.""" + gen_args = setup_for_ssp_fedramp(tmp_trestle_dir, ssp_name) + ssp_gen = SSPGenerate() + assert ssp_gen._run(gen_args) == 0 + + # first assemble + ssp_assemble = f'trestle author ssp-assemble -m {ssp_name} -o {ssp_name} -cd {gen_args.compdefs}' + test_utils.execute_command_and_assert(ssp_assemble, 0, monkeypatch) + + def test_ssp_assemble_remove_comp_defs(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: """Tests the removal of component definitions that are no longer valid for an ssp.""" gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name) @@ -1044,3 +1055,61 @@ def test_ssp_assemble_no_comps(tmp_trestle_dir: pathlib.Path, capsys) -> None: _, err = capsys.readouterr() assert 'Control ac-1 references component Bad Component not defined' in err assert 'Please specify the names of any component-definitions' in err + + +def test_ssp_gen_and_assemble_more_than_one_param(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: + """Test ssp generate and assemble with more than 1 parameters per rule.""" + gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, '', 'comp_def_more_params') + args_compdefs = gen_args.compdefs + + # first create the markdown + ssp_gen = SSPGenerate() + assert ssp_gen._run(gen_args) == 0 + new_version = '1.2.3' + + md_path = tmp_trestle_dir / ssp_name / 'ac' / 'ac-1.md' + assert md_path.exists() + + md_api = MarkdownAPI() + header, tree = md_api.processor.process_markdown(md_path) + rule_parameters = header['x-trestle-comp-def-rules-param-vals']['comp_ca'] + rule_parameters.append({'name': 'allowed_admins_per_account2', 'values': ['20']}) + + md_api.write_markdown_with_header(md_path, header, tree.content.raw_text) + + # verifies a second parameter has beend added to the top shared rule + assert header['x-trestle-rules-params']['comp_ca'][1]['name'] == 'allowed_admins_per_account2' + + # verifies the parameter value for the rule has been written down correctly in the markdown file + assert header['x-trestle-comp-def-rules-param-vals']['comp_ca'][1]['values'] == ['20'] + + # now assemble controls into json ssp + ssp_assemble = SSPAssemble() + args = argparse.Namespace( + trestle_root=tmp_trestle_dir, + markdown=ssp_name, + output=ssp_name, + verbose=0, + regenerate=False, + version=new_version, + name=None, + compdefs=args_compdefs + ) + assert ssp_assemble._run(args) == 0 + + assem_ssp, _ = ModelUtils.load_model_for_class(tmp_trestle_dir, ssp_name, ossp.SystemSecurityPlan) + set_parameters = assem_ssp.control_implementation.implemented_requirements[0].by_components[0].set_parameters + set_params = [ + set_param.param_id for set_param in set_parameters if set_param.param_id == 'allowed_admins_per_account2' + ] + # this demonstrates there's only one iteration of the parameter and not being repeated + assert len(set_params) == 1 + + +def test_ssp_gen_throw_exception_for_rep_comps(tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch) -> None: + """Test ssp generate for duplicated component uuids between diff component definition.""" + gen_args, _ = setup_for_ssp(tmp_trestle_dir, prof_name, ssp_name, False, '', + 'comp_def_more_params,comp_def_more_params_dup') + # first create the markdown + ssp_gen = SSPGenerate() + assert ssp_gen._run(gen_args) == 1 diff --git a/tests/trestle/tasks/csv_to_oscal_cd_test.py b/tests/trestle/tasks/csv_to_oscal_cd_test.py index cabfc541b..37286f5bc 100644 --- a/tests/trestle/tasks/csv_to_oscal_cd_test.py +++ b/tests/trestle/tasks/csv_to_oscal_cd_test.py @@ -470,6 +470,158 @@ def test_execute_bp3_sample(tmp_path: pathlib.Path) -> None: assert ci.set_parameters[3].values[2] == '5' +def test_execute_bp4_sample(tmp_path: pathlib.Path) -> None: + """Test execute bp4 sample.""" + _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') + section['csv-file'] = 'tests/data/csv/bp.sample.v4.csv' + # perform transformation from csv to OSCAL json + tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section) + retval = tgt.execute() + assert retval == TaskOutcome.SUCCESS + # read component-definition + fp = pathlib.Path(tmp_path) / 'component-definition.json' + cd = ComponentDefinition.oscal_read(fp) + # spot check + component = cd.components[0] + assert len(component.props) == 68 + assert len(component.control_implementations) == 1 + # props + index = 0 + assert component.props[index].name == 'Rule_Id' + assert component.props[index].value == 'account_owner_authorized_ip_range_configured' + assert component.props[index].remarks == 'rule_set_00' + index = 7 + assert component.props[index].name == 'Parameter_Id' + assert component.props[index].value == 'allowed_admins_per_account' + assert component.props[index].remarks == 'rule_set_01' + index = 8 + assert component.props[index].name == 'Parameter_Description' + assert component.props[index].value == 'Maximum allowed administrators per' + assert component.props[index].remarks == 'rule_set_01' + index = 9 + assert component.props[index].name == 'Parameter_Value_Alternatives' + assert component.props[index].value == '10' + assert component.props[index].remarks == 'rule_set_01' + index = 10 + assert component.props[index].name == 'Parameter_Id2' + assert component.props[index].value == 'allowed_admins_per_account2' + assert component.props[index].remarks == 'rule_set_01' + index = 11 + assert component.props[index].name == 'Parameter_Description2' + assert component.props[index].value == 'Maximum allowed administrators per2' + assert component.props[index].remarks == 'rule_set_01' + index = 12 + assert component.props[index].name == 'Parameter_Value_Alternatives2' + assert component.props[index].value == '20' + assert component.props[index].remarks == 'rule_set_01' + index = 13 + assert component.props[index].name == 'Check_Id' + assert component.props[index].value == 'iam_admin_role_users_per_account_maxcount' + assert component.props[index].remarks == 'rule_set_01' + # control implementations + ci = component.control_implementations[0] + assert len(ci.set_parameters) == 8 + index = 0 + assert ci.set_parameters[index].param_id == 'allowed_admins_per_account' + assert len(ci.set_parameters[index].values) == 1 + assert ci.set_parameters[index].values[0] == '10' + index = 1 + assert ci.set_parameters[index].param_id == 'allowed_admins_per_account2' + assert len(ci.set_parameters[index].values) == 1 + assert ci.set_parameters[index].values[0] == '20' + index = 2 + assert ci.set_parameters[index].param_id == 'api_keys_rotated_days' + assert len(ci.set_parameters[index].values) == 3 + assert ci.set_parameters[index].values[0] == 'x' + assert ci.set_parameters[index].values[1] == 'y' + assert ci.set_parameters[index].values[2] == 'z' + index = 3 + assert ci.set_parameters[index].param_id == 'api_keys_rotated_days2' + assert len(ci.set_parameters[index].values) == 3 + assert ci.set_parameters[index].values[0] == 'r' + assert ci.set_parameters[index].values[1] == 's' + assert ci.set_parameters[index].values[2] == 't' + + +def test_execute_bp4_modify_additional_param_set(tmp_path: pathlib.Path) -> None: + """Test execute bp4 modify additional param set.""" + _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') + section['csv-file'] = 'tests/data/csv/bp.sample.v4.csv' + # modify additional param set + rows = _get_rows('tests/data/csv/bp.sample.v4.csv') + row = rows[3] + assert row[17] == 'allowed_admins_per_account2' + assert row[19] == '20' + assert row[20] == '20' + row[19] = '50' + row[20] = '45 50 55' + # perform transformation from csv to OSCAL json + with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader: + mock_csv_reader.return_value = rows + tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section) + retval = tgt.execute() + assert retval == TaskOutcome.SUCCESS + # read component-definition + fp = pathlib.Path(tmp_path) / 'component-definition.json' + cd = ComponentDefinition.oscal_read(fp) + # spot check + component = cd.components[0] + assert len(component.props) == 68 + assert len(component.control_implementations) == 1 + # props + index = 0 + assert component.props[index].name == 'Rule_Id' + assert component.props[index].value == 'account_owner_authorized_ip_range_configured' + assert component.props[index].remarks == 'rule_set_00' + index = 12 + assert component.props[index].name == 'Parameter_Value_Alternatives2' + assert component.props[index].value == '45 50 55' + assert component.props[index].remarks == 'rule_set_01' + # control implementations + ci = component.control_implementations[0] + assert len(ci.set_parameters) == 8 + index = 0 + assert ci.set_parameters[index].param_id == 'allowed_admins_per_account' + assert len(ci.set_parameters[index].values) == 1 + assert ci.set_parameters[index].values[0] == '10' + index = 1 + assert ci.set_parameters[index].param_id == 'allowed_admins_per_account2' + assert len(ci.set_parameters[index].values) == 1 + assert ci.set_parameters[index].values[0] == '50' + + +def test_execute_bp4_delete_additional_param_set(tmp_path: pathlib.Path) -> None: + """Test execute bp4 delete additional param set.""" + _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') + section['csv-file'] = 'tests/data/csv/bp.sample.v4.csv' + # delete additional param set + rows = _get_rows('tests/data/csv/bp.sample.v4.csv') + row = rows[3] + assert row[17] == 'allowed_admins_per_account2' + assert row[19] == '20' + assert row[20] == '20' + row[17] = '' + row[18] = '' + row[19] = '' + row[20] = '' + # perform transformation from csv to OSCAL json + with mock.patch('trestle.tasks.csv_to_oscal_cd.csv.reader') as mock_csv_reader: + mock_csv_reader.return_value = rows + tgt = csv_to_oscal_cd.CsvToOscalComponentDefinition(section) + retval = tgt.execute() + assert retval == TaskOutcome.SUCCESS + # read component-definition + fp = pathlib.Path(tmp_path) / 'component-definition.json' + cd = ComponentDefinition.oscal_read(fp) + # spot check + component = cd.components[0] + assert len(component.props) == 65 + assert len(component.control_implementations) == 1 + # control implementations + ci = component.control_implementations[0] + assert len(ci.set_parameters) == 7 + + def test_execute_bp_cd(tmp_path: pathlib.Path) -> None: """Test execute bp cd.""" _, section = _get_config_section_init(tmp_path, 'test-csv-to-oscal-cd-bp.config') diff --git a/trestle/core/catalog/catalog_interface.py b/trestle/core/catalog/catalog_interface.py index 347fd5df5..2a901f1ce 100644 --- a/trestle/core/catalog/catalog_interface.py +++ b/trestle/core/catalog/catalog_interface.py @@ -298,16 +298,20 @@ def get_statement_part_id_map(self, label_as_key: bool) -> Dict[str, Dict[str, s id_map = {} for control in self.get_all_controls_from_catalog(True): statement_part = get_item_from_list(control.parts, const.STATEMENT, lambda p: p.name) - if statement_part: - id_dict: Dict[str, str] = {} - for sub_part in as_list(statement_part.parts): - label = ControlInterface.get_label(sub_part) - if label_as_key: - id_dict[label] = sub_part.id - else: - id_dict[sub_part.id] = label - if id_dict: - id_map[control.id] = id_dict + if not statement_part: + continue + id_dict: Dict[str, str] = {} + for sub_part in as_list(statement_part.parts): + label = ControlInterface.get_label(sub_part) + # skip add to map for empty label + if not label: + continue + if label_as_key: + id_dict[label] = sub_part.id + else: + id_dict[sub_part.id] = label + if id_dict: + id_map[control.id] = id_dict return id_map @staticmethod @@ -748,16 +752,17 @@ def _get_control_memory_info(self, control_id: str, context: ControlContext) -> rules_params = {} rules_param_names = [] for comp_name, rules_params_dict in as_dict(context.rules_params_dict).items(): - for rule_id, rules_param in rules_params_dict.items(): + for rule_id, rules_parameters in rules_params_dict.items(): if rule_id in rule_ids.get(comp_name, []): - param_name = rules_param['name'] - rules_param_names.append(param_name) - rules_param[const.HEADER_RULE_ID] = rule_id_rule_name_map[comp_name].get(rule_id, None) - deep_append(rules_params, [comp_name], rules_param) - deep_set( - param_id_rule_name_map, [comp_name, rules_param['name']], - rule_id_rule_name_map[comp_name][rule_id] - ) + for rule_parameter in rules_parameters: + param_name = rule_parameter['name'] + rules_param_names.append(param_name) + rule_parameter[const.HEADER_RULE_ID] = rule_id_rule_name_map[comp_name].get(rule_id, None) + deep_append(rules_params, [comp_name], rule_parameter) + deep_set( + param_id_rule_name_map, [comp_name, rule_parameter['name']], + rule_id_rule_name_map[comp_name][rule_id] + ) set_or_pop(header, const.RULES_PARAMS_TAG, rules_params) self._extend_rules_param_list(control_id, header, param_id_rule_name_map) @@ -869,12 +874,15 @@ def generate_control_rule_info(self, part_id_map: Dict[str, Dict[str, str]], con """ context.rules_dict = {} context.rules_params_dict = {} + comps_uuids = [] for comp_def_name in context.comp_def_name_list: context.comp_def, _ = ModelUtils.load_model_for_class( context.trestle_root, comp_def_name, comp.ComponentDefinition ) + component_uuids = [comp.uuid for comp in context.comp_def.components] + comps_uuids.extend(component_uuids) for component in as_list(context.comp_def.components): context.component = component context.comp_name = component.title @@ -887,6 +895,14 @@ def generate_control_rule_info(self, part_id_map: Dict[str, Dict[str, str]], con self._add_control_imp_comp_info(context, part_id_map, comp_rules_props) # add the rule_id to the param_dict for param_comp_name, rule_param_dict in context.rules_params_dict.items(): - for rule_tag, param_dict in rule_param_dict.items(): - rule_dict = deep_get(context.rules_dict, [param_comp_name, rule_tag], {}) - param_dict[const.HEADER_RULE_ID] = rule_dict.get(const.NAME, 'unknown_rule') + for rule_tag, params_list in rule_param_dict.items(): + for param in params_list: + rule_dict = deep_get(context.rules_dict, [param_comp_name, rule_tag], {}) + param[const.HEADER_RULE_ID] = rule_dict.get(const.NAME, 'unknown_rule') + # determine if there are duplicated uuids and throw an exception + dup_comp_uuids = set({comp_uuid for comp_uuid in comps_uuids if comps_uuids.count(comp_uuid) > 1}) + if len(dup_comp_uuids) > 0: + # throw an exception if there are repeated component uuids + for comp_uuid in dup_comp_uuids: + logger.error(f'Component uuid { comp_uuid } is duplicated') + raise TrestleError('Component uuids cannot be duplicated between different component definitions') diff --git a/trestle/core/catalog/catalog_writer.py b/trestle/core/catalog/catalog_writer.py index b422e7434..4f7065338 100644 --- a/trestle/core/catalog/catalog_writer.py +++ b/trestle/core/catalog/catalog_writer.py @@ -235,9 +235,10 @@ def _fixup_param_dicts(context: ControlContext) -> None: """Merge info in the rules params dict and the rules param vals dict.""" for comp_name, comp_dict in context.rules_params_dict.items(): rules_dict = context.rules_dict.get(comp_name, {}) - for rule_id, param_dict in comp_dict.items(): - rule_name = deep_get(rules_dict, [rule_id, 'name'], 'unknown_rule_name') - param_dict[const.HEADER_RULE_ID] = rule_name + for rule_id, params_list in comp_dict.items(): + for param in params_list: + rule_name = deep_get(rules_dict, [rule_id, 'name'], 'unknown_rule_name') + param[const.HEADER_RULE_ID] = rule_name def write_catalog_as_ssp_markdown(self, context: ControlContext, part_id_map: Dict[str, Dict[str, str]]) -> None: """ @@ -340,13 +341,13 @@ def _update_values(set_param: comp.SetParameter, control_param_dict) -> None: # get top level rule info applying to all controls comp_rules_dict, comp_rules_params_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(context.component) # noqa E501 context.rules_dict[context.comp_name] = comp_rules_dict - context.rules_params_dict.update(comp_rules_params_dict) + context.rules_params_dict[context.comp_name] = comp_rules_params_dict for control_imp in as_list(context.component.control_implementations): control_imp_rules_dict, control_imp_rules_params_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(control_imp) # noqa E501 context.rules_dict[context.comp_name].update(control_imp_rules_dict) comp_rules_params_dict = context.rules_params_dict.get(context.comp_name, {}) comp_rules_params_dict.update(control_imp_rules_params_dict) - context.rules_params_dict[context.comp_name] = comp_rules_params_dict + context.rules_params_dict[context.comp_name].update(comp_rules_params_dict) ci_set_params = ControlInterface.get_set_params_from_item(control_imp) for imp_req in as_list(control_imp.implemented_requirements): control_part_id_map = part_id_map.get(imp_req.control_id, {}) diff --git a/trestle/core/commands/author/ssp.py b/trestle/core/commands/author/ssp.py index b8868ee51..065e75c2c 100644 --- a/trestle/core/commands/author/ssp.py +++ b/trestle/core/commands/author/ssp.py @@ -320,9 +320,11 @@ def _get_params_for_rules(context: ControlContext, rules_list: List[str], # find param_ids needed by rules for rule_id in rules_list: # get list of param_ids associated with this rule_id - param_ids = [param['name'] for param in rule_dict.values() if param['rule-id'] == rule_id] + param_ids = [ + param['name'] for params in rule_dict.values() for param in params if param['rule-id'] == rule_id + ] needed_param_ids.update(param_ids) - all_rule_param_ids = [param['name'] for param in rule_dict.values()] + all_rule_param_ids = [param['name'] for params in rule_dict.values() for param in params] # any set_param that isn't associated with a rule should be included as a normal control set param with no rule for set_param in set_params: if set_param.param_id not in all_rule_param_ids: diff --git a/trestle/core/control_interface.py b/trestle/core/control_interface.py index 7d363a28b..cd7cc1622 100644 --- a/trestle/core/control_interface.py +++ b/trestle/core/control_interface.py @@ -454,39 +454,50 @@ def get_params_dict_from_item(item: TypeWithProps) -> Tuple[Dict[str, Dict[str, """Get all params found in this item with rule_id as key.""" # id, description, options - where options is a string containing comma-sep list of items # params is dict with rule_id as key and value contains: param_name, description and choices - params: Dict[str, Dict[str, str]] = {} + params: Dict[str, List[Dict[str, str]]] = {} props = [] for prop in as_list(item.props): - if prop.name == const.PARAMETER_ID: + if const.PARAMETER_ID in prop.name: rule_id = prop.remarks param_name = prop.value - if rule_id in params: - raise TrestleError(f'Duplicate param {param_name} found for rule {rule_id}') - # create new param for this rule - params[rule_id] = {'name': param_name} + # rule already exists in parameters dict + if rule_id in params.keys(): + existing_param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None) + if existing_param is not None: + raise TrestleError(f'Param id for rule {rule_id} already exists') + else: + # append a new parameter for the current rule + params[rule_id].append({'name': param_name}) + else: + # create new param for this rule for the first parameter + params[rule_id] = [{'name': param_name}] props.append(prop) - elif prop.name == const.PARAMETER_DESCRIPTION: + elif const.PARAMETER_DESCRIPTION in prop.name: rule_id = prop.remarks if rule_id in params: - params[rule_id]['description'] = prop.value + param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None) + param['description'] = prop.value props.append(prop) else: raise TrestleError(f'Param description for rule {rule_id} found with no param_id') - elif prop.name == const.PARAMETER_VALUE_ALTERNATIVES: + elif const.PARAMETER_VALUE_ALTERNATIVES in prop.name: rule_id = prop.remarks if rule_id in params: - params[rule_id]['options'] = prop.value + param = next((prm for prm in params[rule_id] if prm['name'] == param_name), None) + param['options'] = prop.value props.append(prop) else: raise TrestleError(f'Param options for rule {rule_id} found with no param_id') new_params = {} - for rule_id, param in params.items(): - if 'name' not in param: - logger.warning(f'Parameter for rule_id {rule_id} has no matching name. Ignoring the param.') - else: - param['description'] = param.get('description', '') - param['options'] = param.get('options', '') - new_params[rule_id] = param + for rule_id, rule_params in params.items(): + new_params[rule_id] = [] + for param in rule_params: + if 'name' not in param: + logger.warning(f'Parameter for rule_id {rule_id} has no matching name. Ignoring the param.') + else: + param['description'] = param.get('description', '') + param['options'] = param.get('options', '') + new_params[rule_id].append(param) return new_params, props @staticmethod @@ -1102,14 +1113,16 @@ def insert_imp_req_into_component( """ for control_imp in as_list(component.control_implementations): _, control_imp_param_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(control_imp) - control_imp_rule_param_ids = [d['name'] for d in control_imp_param_dict.values()] + control_imp_rule_param_ids = [ + param['name'] for params in control_imp_param_dict.values() for param in params + ] if profile_title != ModelUtils.get_title_from_model_uri(trestle_root, control_imp.source): continue for imp_req in as_list(control_imp.implemented_requirements): if imp_req.control_id != new_imp_req.control_id: continue _, imp_req_param_dict, _ = ControlInterface.get_rules_and_params_dict_from_item(imp_req) - imp_req_rule_param_ids = [d['name'] for d in imp_req_param_dict] + imp_req_rule_param_ids = [param['name'] for params in imp_req_param_dict.values() for param in params] status = ControlInterface.get_status_from_props(new_imp_req) ControlInterface.insert_status_in_props(imp_req, status) imp_req.description = new_imp_req.description diff --git a/trestle/tasks/csv_to_oscal_cd.py b/trestle/tasks/csv_to_oscal_cd.py index 1a0a11c7f..da863a64e 100644 --- a/trestle/tasks/csv_to_oscal_cd.py +++ b/trestle/tasks/csv_to_oscal_cd.py @@ -56,10 +56,11 @@ CHECK_DESCRIPTION = 'Check_Description' FETCHER_ID = 'Fetcher_Id' FETCHER_DESCRIPTION = 'Fetcher_Description' -PARAMETER_ID = 'Parameter_Id' -PARAMETER_DESCRIPTION = 'Parameter_Description' -PARAMETER_VALUE_DEFAULT = 'Parameter_Value_Default' -PARAMETER_VALUE_ALTERNATIVES = 'Parameter_Value_Alternatives' +PARAMETER = 'Parameter' +PARAMETER_ID = f'{PARAMETER}_Id' +PARAMETER_DESCRIPTION = f'{PARAMETER}_Description' +PARAMETER_VALUE_DEFAULT = f'{PARAMETER}_Value_Default' +PARAMETER_VALUE_ALTERNATIVES = f'{PARAMETER}_Value_Alternatives' validation = 'validation' prefix_rule_set = 'rule_set_' @@ -183,19 +184,17 @@ def print_info(self) -> None: logger.info(text1 + text2 + text3) text1 = ' required columns: ' for text2 in CsvColumn.get_required_column_names(): - if text2 in ['Rule_Description', 'Profile_Source', 'Profile_Description', 'Control_Id_List']: + if text2 in [f'{RULE_DESCRIPTION}', f'{PROFILE_SOURCE}', f'{PROFILE_DESCRIPTION}', f'{CONTROL_ID_LIST}']: text2 += ' (see note 1)' logger.info(text1 + '$$' + text2) text1 = ' ' text1 = ' optional columns: ' for text2 in CsvColumn.get_optional_column_names(): - if text2 in ['Parameter_Id', - 'Parameter_Description', - 'Parameter_Value_Alternatives', - 'Parameter_Value_Default']: - text2 += ' (see note 1)' - if text2 in ['Check_Id', 'Check_Description']: - text2 += ' (see note 2)' + text2 += ' (see note 2)' + logger.info(text1 + '$' + text2) + text1 = ' ' + for text2 in CsvColumn.get_parameter_column_names(): + text2 += ' (see notes 1, 4)' logger.info(text1 + '$' + text2) text1 = ' ' text1 = ' comment columns: ' @@ -229,6 +228,10 @@ def print_info(self) -> None: text1 = ' ' text2 = '[3] column name starting with # causes column to be ignored' logger.info(text1 + text2) + text1 = ' ' + text2 = '[4] additional parameters are specified by adding a common suffix per set' + text3 = f', for example: {PARAMETER_ID}_1, {PARAMETER_DESCRIPTION}_1, ...{PARAMETER_ID}_2...' + logger.info(text1 + text2 + text3) def configure(self) -> bool: """Configure.""" @@ -454,7 +457,7 @@ def _delete_rule_props(self, component: DefinedComponent, rule_id: str) -> List[ for prop in component.props: if prop.remarks != rule_set: props.append(prop) - elif prop.name == PARAMETER_ID: + elif prop.name in self._csv_mgr.get_parameter_id_column_names(): self._delete_rule_set_parameter(component, prop.value) elif prop.name == RULE_ID: self._delete_rule_implemented_requirement(component, prop.value) @@ -559,11 +562,11 @@ def rules_add(self, add_rules: List[str]) -> None: source = self._csv_mgr.get_value(rule_key, PROFILE_SOURCE) description = self._csv_mgr.get_value(rule_key, PROFILE_DESCRIPTION) control_implementation = self._get_control_implementation(component, source, description) - # set-parameter - set_parameter = self._create_set_parameter(rule_key) - if set_parameter: + # set-parameters + set_parameters = self._create_set_parameters(rule_key) + if set_parameters: control_implementation.set_parameters = as_list(control_implementation.set_parameters) - _OscalHelper.add_set_parameter(control_implementation.set_parameters, set_parameter) + _OscalHelper.add_set_parameters(control_implementation.set_parameters, set_parameters) # control-mappings control_mappings = self._csv_mgr.get_value(rule_key, CONTROL_ID_LIST).split() self._add_rule_prop(control_implementation, control_mappings, rule_key) @@ -614,7 +617,7 @@ def _create_rule_props(self, rule_key: tuple) -> List[Property]: rule_set_mgr.add_prop(prop_name, prop_value, namespace, self.get_class(prop_name)) if not self._is_validation(rule_key): # parameter columns - column_names = CsvColumn.get_parameter_column_names() + column_names = self._csv_mgr.get_parameter_column_names() for column_name in column_names: prop_name = self._get_prop_name(column_name) prop_value = self._csv_mgr.get_value(rule_key, column_name).strip() @@ -655,23 +658,26 @@ def _str_to_list(self, value: str) -> List[str]: rval.append(value) return rval - def _create_set_parameter(self, rule_key: tuple) -> SetParameter: - """Create create set parameters.""" - set_parameter = None - name = self._csv_mgr.get_value(rule_key, PARAMETER_ID) - value = self._csv_mgr.get_value(rule_key, PARAMETER_VALUE_DEFAULT) - if name and value: - values = self._str_to_list(value) - set_parameter = SetParameter( - param_id=name, - values=values, - ) - elif name: - row_number = self._csv_mgr.get_row_number(rule_key) - column_name = PARAMETER_VALUE_DEFAULT - text = f'row "{row_number}" missing value for "{column_name}"' - logger.debug(text) - return set_parameter + def _create_set_parameters(self, rule_key: tuple) -> List[SetParameter]: + """Create set parameters.""" + set_parameters = [] + for parameter_id_column_name in self._csv_mgr.get_parameter_id_column_names(): + suffix = parameter_id_column_name.replace(PARAMETER_ID, '') + parameter_value_default_column_name = f'{PARAMETER_VALUE_DEFAULT}{suffix}' + name = self._csv_mgr.get_value(rule_key, parameter_id_column_name) + value = self._csv_mgr.get_value(rule_key, parameter_value_default_column_name) + if name and value: + values = self._str_to_list(value) + set_parameter = SetParameter( + param_id=name, + values=values, + ) + set_parameters.append(set_parameter) + elif name: + row_number = self._csv_mgr.get_row_number(rule_key) + text = f'row "{row_number}" missing value for "{parameter_value_default_column_name}"' + logger.debug(text) + return set_parameters def _get_implemented_requirement( self, control_implementation: ControlImplementation, control_id: str @@ -730,7 +736,7 @@ def _modify_rule_props(self, component: DefinedComponent, rule_key: tuple) -> Li class_ = self.get_class(column_name) self._cd_mgr.update_rule_definition(component, rule_set, column_name, column_value, rule_ns, class_) # parameter columns - column_names = CsvColumn.get_parameter_column_names() + column_names = self._csv_mgr.get_parameter_column_names() for column_name in column_names: column_value = self._csv_mgr.get_value(rule_key, column_name).strip() class_ = self.get_class(column_name) @@ -778,7 +784,7 @@ def set_params_add(self, add_set_params: List[str]) -> None: control_implementation.set_parameters = as_list(control_implementation.set_parameters) # add rule_key = _CsvMgr.get_rule_key(component_title, component_type, rule_id) - values = [self._csv_mgr.get_value(rule_key, PARAMETER_VALUE_DEFAULT)] + values = [self._csv_mgr.get_default_value_by_id(rule_key, param_id)] set_parameter = SetParameter( param_id=param_id, values=values, @@ -803,7 +809,7 @@ def set_params_mod(self, mod_set_params: List[str]) -> None: if set_parameter.param_id != param_id: continue rule_key = _CsvMgr.get_rule_key(component_title, component_type, rule_id) - values = [self._csv_mgr.get_value(rule_key, PARAMETER_VALUE_DEFAULT)] + values = [self._csv_mgr.get_default_value_by_id(rule_key, param_id)] replacement = SetParameter( param_id=param_id, values=values, @@ -888,6 +894,12 @@ def control_mappings_add(self, add_control_mappings: List[str]) -> None: class _OscalHelper(): """Oscal Helper.""" + @staticmethod + def add_set_parameters(set_parameter_list: List[SetParameter], set_parameter_list_add: List[SetParameter]) -> None: + """Add set parameters.""" + for set_parameter in set_parameter_list_add: + _OscalHelper.add_set_parameter(set_parameter_list, set_parameter) + @staticmethod def add_set_parameter(set_parameter_list: List[SetParameter], set_parameter: SetParameter) -> None: """Add set parameter.""" @@ -983,16 +995,25 @@ def add_prop(self, name: str, value: str, ns: str, class_: str) -> None: def get_props(self) -> List[Property]: """Get props.""" rval = [] + # required c1 = CsvColumn.get_required_column_names() for key in c1: if key in self._props.keys(): rval.append(self._props[key]) + # parameter + c3 = [] + for key in self._props.keys(): + if key.startswith(f'{PARAMETER}'): + rval.append(self._props[key]) + c3.append(key) + # optional c2 = CsvColumn.get_optional_column_names() for key in c2: if key in self._props.keys(): rval.append(self._props[key]) + # user for key in self._props.keys(): - if key in c1 or key in c2: + if key in c1 or key in c2 or key in c3: continue rval.append(self._props[key]) return rval @@ -1333,15 +1354,6 @@ class CsvColumn(): f'{NAMESPACE}', ] - _columns_optional = [ - f'{PARAMETER_ID}', - f'{PARAMETER_DESCRIPTION}', - f'{PARAMETER_VALUE_ALTERNATIVES}', - f'{PARAMETER_VALUE_DEFAULT}', - f'{CHECK_ID}', - f'{CHECK_DESCRIPTION}', - ] - _columns_required_validation = [ f'{COMPONENT_TITLE}', f'{COMPONENT_DESCRIPTION}', @@ -1352,15 +1364,46 @@ class CsvColumn(): f'{CHECK_DESCRIPTION}', ] + _columns_optional = [ + f'{CHECK_ID}', + f'{CHECK_DESCRIPTION}', + ] + + _columns_parameter = [ + f'{PARAMETER_ID}', + f'{PARAMETER_DESCRIPTION}', + f'{PARAMETER_VALUE_ALTERNATIVES}', + f'{PARAMETER_VALUE_DEFAULT}', + ] + + _columns_ordered = _columns_required + _columns_parameter + _columns_optional + @staticmethod def get_order(column_name: str) -> int: """Get order for column_name.""" rval = sys.maxsize - columns_ordered = CsvColumn._columns_required + CsvColumn._columns_optional - if column_name in columns_ordered: - rval = columns_ordered.index(column_name) + if column_name in CsvColumn._columns_ordered: + rval = CsvColumn._columns_ordered.index(column_name) return rval + @staticmethod + def is_column_name_required(name: str) -> bool: + """Is column name required.""" + return name in (CsvColumn._columns_required + CsvColumn._columns_required_validation) + + @staticmethod + def is_column_name_optional(name: str) -> bool: + """Is column name optional.""" + return name in (CsvColumn._columns_optional) + + @staticmethod + def is_column_name_parameter(name: str) -> bool: + """Is column name parameter.""" + for cname in CsvColumn._columns_parameter: + if name.startswith(cname): + return True + return False + @staticmethod def get_required_column_names() -> List[str]: """Get required column names.""" @@ -1376,11 +1419,10 @@ def get_optional_column_names() -> List[str]: return rval @staticmethod - def get_reserved_column_names() -> List[str]: - """Get reserved column names.""" + def get_parameter_column_names() -> List[str]: + """Get parameter column names.""" rval = [] - rval += CsvColumn._columns_required - rval += CsvColumn._columns_optional + rval += CsvColumn._columns_parameters return rval @staticmethod @@ -1417,12 +1459,7 @@ def get_rule_property_column_names() -> List[str]: ] # optional columns which do not become properties, initially - _columns_optional_filtered = [ - f'{PARAMETER_ID}', - f'{PARAMETER_DESCRIPTION}', - f'{PARAMETER_VALUE_ALTERNATIVES}', - f'{PARAMETER_VALUE_DEFAULT}', - ] + _columns_optional_filtered = [] _columns_filtered = _columns_required_filtered + _columns_optional_filtered @@ -1462,13 +1499,6 @@ def get_check_property_column_names() -> List[str]: f'{PARAMETER_VALUE_ALTERNATIVES}', ] - @staticmethod - def get_parameter_column_names() -> List[str]: - """Get parameter column names.""" - rval = [] - rval += CsvColumn._columns_parameters - return rval - # optional columns which require Param_Id be present in the row _columns_parameters_dependent = [ f'{PARAMETER_DESCRIPTION}', @@ -1477,6 +1507,9 @@ def get_parameter_column_names() -> List[str]: ] +Row = Iterator[List[str]] + + class _CsvMgr(): """Csv Manager.""" @@ -1511,32 +1544,64 @@ def __init__(self, csv_path: pathlib.Path) -> None: if source and source not in self._csv_profile_list: self._csv_profile_list.append(source) description = self.get_row_value(row, PROFILE_DESCRIPTION) - param_id = self.get_row_value(row, PARAMETER_ID) - if param_id: - key = (component_title, component_type, rule_id, source, description, param_id) - self._csv_set_params_map[key] = [row_num, row] - logger.debug(f'csv-set-parameters: {key} {self._csv_set_params_map[key][0]}') + for param_id_key in self.get_parameter_id_column_names(): + param_id = self.get_row_value(row, param_id_key) + if param_id: + key = (component_title, component_type, rule_id, source, description, param_id) + self._csv_set_params_map[key] = [row_num, row] + logger.debug(f'csv-set-parameters: {key} {self._csv_set_params_map[key][0]}') # control mappings - control_mappings = self.get_row_value(row, CONTROL_ID_LIST) - if control_mappings: - controls = control_mappings.split() - for control in controls: - key = (component_description, component_type, rule_id, source, description, control) - self._csv_controls_map[key] = [row_num, row] + self._control_mappings(row_num, row, component_description, component_type, rule_id, source, description) logger.debug(f'csv rules: {len(self._csv_rules_map)}') logger.debug(f'csv params: {len(self._csv_set_params_map)}') logger.debug(f'csv controls: {len(self._csv_controls_map)}') + def _control_mappings( + self, + row_num: int, + row: Row, + component_description: str, + component_type: str, + rule_id: str, + source: str, + description: str + ) -> None: + """Control_mappings.""" + control_mappings = self.get_row_value(row, CONTROL_ID_LIST) + if control_mappings: + controls = control_mappings.split() + for control in controls: + key = (component_description, component_type, rule_id, source, description, control) + self._csv_controls_map[key] = [row_num, row] + @staticmethod def get_rule_key(component_title: str, component_type: str, rule_id: str) -> tuple: """Get rule_key.""" return (component_title, component_type, rule_id) + def get_parameter_id_column_names(self) -> List[str]: + """Get parameter_id column_names.""" + col_names = [] + for col_name in self._csv[0]: + if col_name.startswith(PARAMETER_ID): + col_names.append(col_name) + return col_names + + def get_parameter_column_names(self) -> List[str]: + """Get parameter column_names.""" + col_names = [] + for col_name in self._csv[0]: + if col_name.startswith(PARAMETER_VALUE_DEFAULT): + continue + if col_name.startswith(PARAMETER): + col_names.append(col_name) + return col_names + def get_profile_list(self): """Get profile list.""" return [] + self._csv_profile_list - def row_generator(self) -> Generator[Union[int, Iterator[List[str]]], None, None]: + def row_generator(self) -> Generator[Union[int, Row], None, None]: """Generate rows.""" index = 0 for row in self._csv: @@ -1652,11 +1717,25 @@ def get_value(self, rule_key: tuple, name: str) -> str: row = self.get_row(rule_key) return self.get_row_value(row, name) + def get_default_value_by_id(self, rule_key: tuple, name: str) -> str: + """Get default value for specified parameter id.""" + rval = None + for parameter_id_column_name in self.get_parameter_id_column_names(): + parameter_id_name = self.get_value(rule_key, parameter_id_column_name) + if name == parameter_id_name: + suffix = parameter_id_column_name.replace(PARAMETER_ID, '') + parameter_value_default_column_name = f'{PARAMETER_VALUE_DEFAULT}{suffix}' + rval = self.get_value(rule_key, parameter_value_default_column_name) + break + return rval + def get_user_column_names(self) -> List[str]: """Get user column names.""" user_column_names = [] - reserved_column_names = CsvColumn.get_reserved_column_names() for column_name in self._csv[0]: - if column_name not in reserved_column_names: + t1 = CsvColumn.is_column_name_required(column_name) + t2 = CsvColumn.is_column_name_optional(column_name) + t3 = CsvColumn.is_column_name_parameter(column_name) + if not (t1 or t2 or t3): user_column_names.append(column_name) return user_column_names