diff options
-rw-r--r-- | internal/cvss/v2key_test.go | 98 | ||||
-rw-r--r-- | internal/cvss/v2metric.go | 2 | ||||
-rw-r--r-- | internal/cvss/v2metric_test.go | 267 | ||||
-rw-r--r-- | internal/cvss/v2vector.go | 2 | ||||
-rw-r--r-- | internal/cvss/v2vector_test.go | 48 | ||||
-rw-r--r-- | internal/cvss/v30vector_test.go | 49 | ||||
-rw-r--r-- | internal/cvss/v31vector_test.go | 52 | ||||
-rw-r--r-- | internal/cvss/v3key_test.go | 124 |
8 files changed, 622 insertions, 20 deletions
diff --git a/internal/cvss/v2key_test.go b/internal/cvss/v2key_test.go new file mode 100644 index 0000000..76a3d79 --- /dev/null +++ b/internal/cvss/v2key_test.go @@ -0,0 +1,98 @@ +package cvss + +import "testing" + +func TestV2KeyString(t *testing.T) { + tests := []struct { + key v2Key + exp string + } { + { v2AccessVector, "AV" }, + { v2AccessComplexity, "AC" }, + { v2Authentication, "Au" }, + { v2ConfidentialityImpact, "C" }, + { v2IntegrityImpact, "I" }, + { v2AvailabilityImpact, "A" }, + { v2Exploitability, "E" }, + { v2RemediationLevel, "RL" }, + { v2ReportConfidence, "RC" }, + { v2CollateralDamagePotential, "CDP" }, + { v2TargetDistribution, "TD" }, + { v2ConfidentialityRequirement, "CR" }, + { v2IntegrityRequirement, "IR" }, + { v2AvailabilityRequirement, "AR" }, + + { v2Key(255), "v2Key(255)" }, + } + + for _, test := range(tests) { + t.Run(test.exp, func(t *testing.T) { + got := test.key.String() + if got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } + }) + } +} + +func TestV2KeyName(t *testing.T) { + tests := []struct { + key v2Key + exp string + } { + { v2AccessVector, "Access Vector" }, + { v2AccessComplexity, "Access Complexity" }, + { v2Authentication, "Authentication" }, + { v2ConfidentialityImpact, "Confidentiality Impact" }, + { v2IntegrityImpact, "Integrity Impact" }, + { v2AvailabilityImpact, "Availability Impact" }, + { v2Exploitability, "Exploitability" }, + { v2RemediationLevel, "Remediation Level" }, + { v2ReportConfidence, "Report Confidence" }, + { v2CollateralDamagePotential, "Collateral Damage Potential" }, + { v2TargetDistribution, "Target Distribution" }, + { v2ConfidentialityRequirement, "Confidentiality Requirement" }, + { v2IntegrityRequirement, "Integrity Requirement" }, + { v2AvailabilityRequirement, "Availability Requirement" }, + } + + for _, test := range(tests) { + t.Run(test.exp, func(t *testing.T) { + got := test.key.Name() + if got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } + }) + } +} + +func TestV2KeyCategory(t *testing.T) { + tests := []struct { + key v2Key + exp Category + } { + { v2AccessVector, Base }, + { v2AccessComplexity, Base }, + { v2Authentication, Base }, + { v2ConfidentialityImpact, Base }, + { v2IntegrityImpact, Base }, + { v2AvailabilityImpact, Base }, + { v2Exploitability, Temporal }, + { v2RemediationLevel, Temporal }, + { v2ReportConfidence, Temporal }, + { v2CollateralDamagePotential, Environmental }, + { v2TargetDistribution, Environmental}, + { v2ConfidentialityRequirement, Environmental}, + { v2IntegrityRequirement, Environmental}, + { v2AvailabilityRequirement, Environmental}, + } + + for _, test := range(tests) { + t.Run(test.key.String(), func(t *testing.T) { + got := test.key.Category() + if got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } + }) + } +} diff --git a/internal/cvss/v2metric.go b/internal/cvss/v2metric.go index de8b52b..a80cf8a 100644 --- a/internal/cvss/v2metric.go +++ b/internal/cvss/v2metric.go @@ -223,7 +223,7 @@ var v2MetricStrLut = map[string]v2Metric { } // Convert string to CVSS 2.0 metric. -func getV2MetricFromString(s string) (v2Metric, error) { +func getV2Metric(s string) (v2Metric, error) { // get metric m, ok := v2MetricStrLut[s] if !ok { diff --git a/internal/cvss/v2metric_test.go b/internal/cvss/v2metric_test.go new file mode 100644 index 0000000..b17d2ed --- /dev/null +++ b/internal/cvss/v2metric_test.go @@ -0,0 +1,267 @@ +package cvss + +import "testing" + +func TestGetV2Metric(t *testing.T) { + tests := []struct { + val string + exp v2Metric + ok bool + } { + { "AV:N", v2AVNetwork, true }, + { "AV:A", v2AVAdjacentNetwork, true }, + { "AV:L", v2AVLocal, true }, + + { "AC:L", v2ACLow, true }, + { "AC:M", v2ACMedium, true }, + { "AC:H", v2ACHigh, true }, + + { "Au:M", v2AuMultiple, true }, + { "Au:S", v2AuSingle, true }, + { "Au:N", v2AuNone, true }, + + { "C:N", v2CNone, true }, + { "C:P", v2CPartial, true }, + { "C:C", v2CComplete, true }, + + { "I:N", v2INone, true }, + { "I:P", v2IPartial, true }, + { "I:C", v2IComplete, true }, + + { "A:N", v2ANone, true }, + { "A:P", v2APartial, true }, + { "A:C", v2AComplete, true }, + + { "E:ND", v2ENotDefined, true }, + { "E:U", v2EUnproven, true }, + { "E:POC", v2EProofOfConcept, true }, + { "E:F", v2EFunctional, true }, + { "E:H", v2EHigh, true }, + + { "RL:OF", v2RLOfficialFix, true }, + { "RL:TF", v2RLTemporaryFix, true }, + { "RL:W", v2RLWorkaround, true }, + { "RL:U", v2RLUnavailable, true }, + { "RL:ND", v2RLNotDefined, true }, + + { "RC:UC", v2RCUnconfirmed, true }, + { "RC:UR", v2RCUncorroborated, true }, + { "RC:C", v2RCConfirmed, true }, + { "RC:ND", v2RCNotDefined, true }, + + { "CDP:N", v2CDPNone, true }, + { "CDP:L", v2CDPLow, true }, + { "CDP:LM", v2CDPLowMedium, true }, + { "CDP:MH", v2CDPMediumHigh, true }, + { "CDP:H", v2CDPHigh, true }, + { "CDP:ND", v2CDPNotDefined, true }, + + { "TD:N", v2TDNone, true }, + { "TD:L", v2TDLow, true }, + { "TD:M", v2TDMedium, true }, + { "TD:H", v2TDHigh, true }, + { "TD:ND", v2TDNotDefined, true }, + + { "CR:L", v2CRLow, true }, + { "CR:M", v2CRMedium, true }, + { "CR:H", v2CRHigh, true }, + { "CR:ND", v2CRNotDefined, true }, + + { "IR:L", v2IRLow, true }, + { "IR:M", v2IRMedium, true }, + { "IR:H", v2IRHigh, true }, + { "IR:ND", v2IRNotDefined, true }, + + { "AR:L", v2ARLow, true }, + { "AR:M", v2ARMedium, true }, + { "AR:H", v2ARHigh, true }, + { "AR:ND", v2ARNotDefined, true }, + + { "asdf", v2InvalidMetric, false }, + } + + for _, test := range(tests) { + t.Run(test.val, func(t *testing.T) { + got, err := getV2Metric(test.val) + if test.ok && err == nil && got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } else if test.ok && err != nil { + t.Error(err) + } else if !test.ok && err == nil { + t.Errorf("got: \"%s\", exp: error", got) + } + }) + } +} + +func TestGetV2MetricKey(t *testing.T) { + tests := []struct { + val v2Metric + exp v2Key + } { + { v2AVNetwork, v2AccessVector }, + { v2AVAdjacentNetwork, v2AccessVector }, + { v2AVLocal, v2AccessVector }, + + { v2ACLow, v2AccessComplexity }, + { v2ACMedium, v2AccessComplexity }, + { v2ACHigh, v2AccessComplexity }, + + { v2AuMultiple, v2Authentication }, + { v2AuSingle, v2Authentication }, + { v2AuNone, v2Authentication }, + + { v2CNone, v2ConfidentialityImpact }, + { v2CPartial, v2ConfidentialityImpact }, + { v2CComplete, v2ConfidentialityImpact }, + + { v2INone, v2IntegrityImpact }, + { v2IPartial, v2IntegrityImpact }, + { v2IComplete, v2IntegrityImpact }, + + { v2ANone, v2AvailabilityImpact }, + { v2APartial, v2AvailabilityImpact }, + { v2AComplete, v2AvailabilityImpact }, + + { v2ENotDefined, v2Exploitability }, + { v2EUnproven, v2Exploitability }, + { v2EProofOfConcept, v2Exploitability }, + { v2EFunctional, v2Exploitability }, + { v2EHigh, v2Exploitability }, + + { v2RLOfficialFix, v2RemediationLevel }, + { v2RLTemporaryFix, v2RemediationLevel }, + { v2RLWorkaround, v2RemediationLevel }, + { v2RLUnavailable, v2RemediationLevel }, + { v2RLNotDefined, v2RemediationLevel }, + + { v2RCUnconfirmed, v2ReportConfidence }, + { v2RCUncorroborated, v2ReportConfidence }, + { v2RCConfirmed, v2ReportConfidence }, + { v2RCNotDefined, v2ReportConfidence }, + + { v2CDPNone, v2CollateralDamagePotential }, + { v2CDPLow, v2CollateralDamagePotential }, + { v2CDPLowMedium, v2CollateralDamagePotential }, + { v2CDPMediumHigh, v2CollateralDamagePotential }, + { v2CDPHigh, v2CollateralDamagePotential }, + { v2CDPNotDefined, v2CollateralDamagePotential }, + + { v2TDNone, v2TargetDistribution }, + { v2TDLow, v2TargetDistribution }, + { v2TDMedium, v2TargetDistribution }, + { v2TDHigh, v2TargetDistribution }, + { v2TDNotDefined, v2TargetDistribution }, + + { v2CRLow, v2ConfidentialityRequirement }, + { v2CRMedium, v2ConfidentialityRequirement }, + { v2CRHigh, v2ConfidentialityRequirement }, + { v2CRNotDefined, v2ConfidentialityRequirement }, + + { v2IRLow, v2IntegrityRequirement }, + { v2IRMedium, v2IntegrityRequirement }, + { v2IRHigh, v2IntegrityRequirement }, + { v2IRNotDefined, v2IntegrityRequirement }, + + { v2ARLow, v2AvailabilityRequirement }, + { v2ARMedium, v2AvailabilityRequirement }, + { v2ARHigh, v2AvailabilityRequirement }, + { v2ARNotDefined, v2AvailabilityRequirement }, + } + + for _, test := range(tests) { + t.Run(test.val.String(), func(t *testing.T) { + got := test.val.Key() + if got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } + }) + } +} + +func TestV2MetricString(t *testing.T) { + tests := []struct { + val v2Metric + exp string + } { + { v2AVNetwork, "AV:N" }, + { v2AVAdjacentNetwork, "AV:A" }, + { v2AVLocal, "AV:L" }, + + { v2ACLow, "AC:L" }, + { v2ACMedium, "AC:M" }, + { v2ACHigh, "AC:H" }, + + { v2AuMultiple, "Au:M" }, + { v2AuSingle, "Au:S" }, + { v2AuNone, "Au:N" }, + + { v2CNone, "C:N" }, + { v2CPartial, "C:P" }, + { v2CComplete, "C:C" }, + + { v2INone, "I:N" }, + { v2IPartial, "I:P" }, + { v2IComplete, "I:C" }, + + { v2ANone, "A:N" }, + { v2APartial, "A:P" }, + { v2AComplete, "A:C" }, + + { v2ENotDefined, "E:ND" }, + { v2EUnproven, "E:U" }, + { v2EProofOfConcept, "E:POC" }, + { v2EFunctional, "E:F" }, + { v2EHigh, "E:H" }, + + { v2RLOfficialFix, "RL:OF" }, + { v2RLTemporaryFix, "RL:TF" }, + { v2RLWorkaround, "RL:W" }, + { v2RLUnavailable, "RL:U" }, + { v2RLNotDefined, "RL:ND" }, + + { v2RCUnconfirmed, "RC:UC" }, + { v2RCUncorroborated, "RC:UR" }, + { v2RCConfirmed, "RC:C" }, + { v2RCNotDefined, "RC:ND" }, + + { v2CDPNone, "CDP:N" }, + { v2CDPLow, "CDP:L" }, + { v2CDPLowMedium, "CDP:LM" }, + { v2CDPMediumHigh, "CDP:MH" }, + { v2CDPHigh, "CDP:H" }, + { v2CDPNotDefined, "CDP:ND" }, + + { v2TDNone, "TD:N" }, + { v2TDLow, "TD:L" }, + { v2TDMedium, "TD:M" }, + { v2TDHigh, "TD:H" }, + { v2TDNotDefined, "TD:ND" }, + + { v2CRLow, "CR:L" }, + { v2CRMedium, "CR:M" }, + { v2CRHigh, "CR:H" }, + { v2CRNotDefined, "CR:ND" }, + + { v2IRLow, "IR:L" }, + { v2IRMedium, "IR:M" }, + { v2IRHigh, "IR:H" }, + { v2IRNotDefined, "IR:ND" }, + + { v2ARLow, "AR:L" }, + { v2ARMedium, "AR:M" }, + { v2ARHigh, "AR:H" }, + { v2ARNotDefined, "AR:ND" }, + + { v2Metric(255), "v2Metric(255)" }, + } + + for _, test := range(tests) { + t.Run(test.val.String(), func(t *testing.T) { + got := test.val.String() + if got != test.exp { + t.Errorf("got: \"%s\", exp: \"%s\"", got, test.exp) + } + }) + } +} diff --git a/internal/cvss/v2vector.go b/internal/cvss/v2vector.go index efa2444..909bca6 100644 --- a/internal/cvss/v2vector.go +++ b/internal/cvss/v2vector.go @@ -47,7 +47,7 @@ func newV2Vector(s string) (Vector, error) { // walk metric strings for i, ms := range(strs) { // convert string to vector - m, err := getV2MetricFromString(ms) + m, err := getV2Metric(ms) if err != nil { return nil, err } diff --git a/internal/cvss/v2vector_test.go b/internal/cvss/v2vector_test.go index 3ef4194..a943957 100644 --- a/internal/cvss/v2vector_test.go +++ b/internal/cvss/v2vector_test.go @@ -1,17 +1,51 @@ package cvss -import "testing" +import ( + "testing" +) -// test v2 vector -var testCvssV2 = "AV:L/AC:L/Au:N/C:N/I:N/A:P" - -// Test cvss v2 parser +// Test cvss v2 func TestCvssV2Parser(t *testing.T) { + // test v2 vector + test := "AV:L/AC:L/Au:N/C:N/I:N/A:P" + expMetrics := []string { "AV:L", "AC:L", "Au:N", "C:N", "I:N", "A:P" } + t.Run("TestV2Parser", func(t *testing.T) { - // parse vector, check for error - _, err := NewVector(testCvssV2) + // parse vector + vec, err := NewVector(test) if err != nil { t.Error(err) + return + } + + // check version + if vec.Version() != V20 { + t.Errorf("got %s, exp %s", vec.Version(), V20) + return + } + + for i, m := range(vec.Metrics()) { + got := m.String() + if got != expMetrics[i] { + t.Errorf("got %s, exp %s", got, expMetrics[i]) + } } }) } + +// Test cvss v2 +func TestInvalidV2Vector(t *testing.T) { + // test invalid vector + test := "AV:L/junk/Au:N/C:N/I:N/A:P" + exp := "invalid CVSS 2.0 metric: junk" + + // parse vector + got, err := NewVector(test) + if err != nil && err.Error() != exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), err) + return + } else if err == nil { + t.Errorf("got \"%s\", exp badMetric", got) + return + } +} diff --git a/internal/cvss/v30vector_test.go b/internal/cvss/v30vector_test.go new file mode 100644 index 0000000..fae3436 --- /dev/null +++ b/internal/cvss/v30vector_test.go @@ -0,0 +1,49 @@ +package cvss + +import ( + "testing" +) + +func TestV30Vector(t *testing.T) { + // test v3 vector + test := "CVSS:3.0/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + expMetrics := []string { "AV:L", "AC:H", "PR:L", "UI:N", "S:U", "C:H", "I:N", "A:N" } + + t.Run("TestV30Vector", func(t *testing.T) { + // parse vector + vec, err := NewVector(test) + if err != nil { + t.Error(err) + return + } + + // check version + if vec.Version() != V30 { + t.Errorf("got %s, exp %s", vec.Version(), V30) + return + } + + for i, m := range(vec.Metrics()) { + got := m.String() + if got != expMetrics[i] { + t.Errorf("got %s, exp %s", got, expMetrics[i]) + } + } + }) +} + +func TestInvalidV30Vector(t *testing.T) { + // test invalid vector + test := "CVSS:3.0/AV:L/junk/PR:L/UI:N/S:U/C:H/I:N/A:N" + exp := "invalid CVSS 3.0 metric: junk" + + // parse vector + got, err := NewVector(test) + if err != nil && err.Error() != exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), err) + return + } else if err == nil { + t.Errorf("got \"%s\", exp badMetric", got) + return + } +} diff --git a/internal/cvss/v31vector_test.go b/internal/cvss/v31vector_test.go index 8340123..ba82edb 100644 --- a/internal/cvss/v31vector_test.go +++ b/internal/cvss/v31vector_test.go @@ -1,19 +1,49 @@ package cvss -import "testing" +import ( + "testing" +) -// test cvss v3.1 vector -var testCvssV3 = "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H" +func TestV31Vector(t *testing.T) { + // test v3 vector + test := "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + expMetrics := []string { "AV:L", "AC:H", "PR:L", "UI:N", "S:U", "C:H", "I:N", "A:N" } -// Test CVSS v3 parser. -func TestCvssV3Parser(t *testing.T) { - t.Run("TestV3", func(t *testing.T) { - // parse vector, check for error - if _, err := NewVector(testCvssV3); err != nil { - t.Fatal(err) + t.Run("TestV31Vector", func(t *testing.T) { + // parse vector + vec, err := NewVector(test) + if err != nil { + t.Error(err) + return + } + + // check version + if vec.Version() != V31 { + t.Errorf("got %s, exp %s", vec.Version(), V31) + return + } + + for i, m := range(vec.Metrics()) { + got := m.String() + if got != expMetrics[i] { + t.Errorf("got %s, exp %s", got, expMetrics[i]) + } } }) +} + +func TestInvalidV31Vector(t *testing.T) { + // test invalid vector + test := "CVSS:3.1/AV:L/junk/PR:L/UI:N/S:U/C:H/I:N/A:N" + exp := "invalid CVSS 3.1 metric: junk" - // fmt.Println(v.Metrics()) - // fmt.Println(v) + // parse vector + got, err := NewVector(test) + if err != nil && err.Error() != exp { + t.Errorf("got \"%s\", exp \"%s\"", err.Error(), err) + return + } else if err == nil { + t.Errorf("got \"%s\", exp badMetric", got) + return + } } diff --git a/internal/cvss/v3key_test.go b/internal/cvss/v3key_test.go new file mode 100644 index 0000000..ccf2b8f --- /dev/null +++ b/internal/cvss/v3key_test.go @@ -0,0 +1,124 @@ +package cvss + +import ( + "testing" +) + +func TestV3KeyString(t *testing.T) { + tests := []struct { + val v3Key + exp string + } { + { v3AttackVector, "AV" }, + { v3AttackComplexity, "AC" }, + { v3PrivilegesRequired, "PR" }, + { v3UserInteraction, "UI" }, + { v3Scope, "S" }, + { v3Confidentiality, "C" }, + { v3Integrity, "I" }, + { v3Availability, "A" }, + { v3ExploitCodeMaturity, "E" }, + { v3RemediationLevel, "RL" }, + { v3ReportConfidence, "RC" }, + { v3ConfidentialityRequirement, "CR" }, + { v3IntegrityRequirement, "IR" }, + { v3AvailabilityRequirement, "AR" }, + { v3ModifiedAttackVector, "MAV" }, + { v3ModifiedAttackComplexity, "MAC" }, + { v3ModifiedPrivilegesRequired, "MPR" }, + { v3ModifiedUserInteraction, "MUI" }, + { v3ModifiedScope, "MS" }, + { v3ModifiedConfidentiality, "MC" }, + { v3ModifiedIntegrity, "MI" }, + { v3ModifiedAvailability, "MA" }, + + { v3Key(255), "v3Key(255)" }, + } + + for _, test := range(tests) { + t.Run(test.exp, func(t *testing.T) { + got := test.val.String() + if got != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", got, test.exp) + } + }) + } +} + +func TestV3KeyName(t *testing.T) { + tests := []struct { + val v3Key + exp string + } { + { v3AttackVector, "Attack Vector" }, + { v3AttackComplexity, "Attack Complexity" }, + { v3PrivilegesRequired, "Privileges Required" }, + { v3UserInteraction, "User Interaction" }, + { v3Scope, "Scope" }, + { v3Confidentiality, "Confidentiality" }, + { v3Integrity, "Integrity" }, + { v3Availability, "Availability" }, + { v3ExploitCodeMaturity, "Exploit Code Maturity" }, + { v3RemediationLevel, "Remediation Level" }, + { v3ReportConfidence, "Report Confidence" }, + { v3ConfidentialityRequirement, "Confidentiality Requirement" }, + { v3IntegrityRequirement, "Integrity Requirement" }, + { v3AvailabilityRequirement, "Availability Requirement" }, + { v3ModifiedAttackVector, "Modified Attack Vector" }, + { v3ModifiedAttackComplexity, "Modified Attack Complexity" }, + { v3ModifiedPrivilegesRequired, "Modified Privileges Required" }, + { v3ModifiedUserInteraction, "Modified User Interaction" }, + { v3ModifiedScope, "Modified Scope" }, + { v3ModifiedConfidentiality, "Modified Confidentiality" }, + { v3ModifiedIntegrity, "Modified Integrity" }, + { v3ModifiedAvailability, "Modified Availability" }, + } + + for _, test := range(tests) { + t.Run(test.exp, func(t *testing.T) { + got := test.val.Name() + if got != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", got, test.exp) + } + }) + } +} + +func TestV3KeyCategory(t *testing.T) { + tests := []struct { + val v3Key + exp Category + } { + { v3AttackVector, Base }, + { v3AttackComplexity, Base }, + { v3PrivilegesRequired, Base }, + { v3UserInteraction, Base }, + { v3Scope, Base }, + { v3Confidentiality, Base }, + { v3Integrity, Base }, + { v3Availability, Base }, + { v3ExploitCodeMaturity, Temporal }, + { v3RemediationLevel, Temporal }, + { v3ReportConfidence, Temporal }, + { v3ConfidentialityRequirement, Environmental }, + { v3IntegrityRequirement, Environmental }, + { v3AvailabilityRequirement, Environmental }, + { v3ModifiedAttackVector, Environmental }, + { v3ModifiedAttackComplexity, Environmental }, + { v3ModifiedPrivilegesRequired, Environmental }, + { v3ModifiedUserInteraction, Environmental }, + { v3ModifiedScope, Environmental }, + { v3ModifiedConfidentiality, Environmental }, + { v3ModifiedIntegrity, Environmental }, + { v3ModifiedAvailability, Environmental }, + } + + for _, test := range(tests) { + t.Run(test.val.String(), func(t *testing.T) { + got := test.val.Category() + if got != test.exp { + t.Errorf("got \"%s\", exp \"%s\"", got, test.exp) + } + }) + } +} |