diff --git a/scubagoggles/Testing/Unit/Rego/gmail/gmail03_test.rego b/scubagoggles/Testing/Unit/Rego/gmail/gmail03_test.rego
index 0be880403..f5dce019c 100644
--- a/scubagoggles/Testing/Unit/Rego/gmail/gmail03_test.rego
+++ b/scubagoggles/Testing/Unit/Rego/gmail/gmail03_test.rego
@@ -152,4 +152,24 @@ test_SPF_Incorrect_V3 if {
not RuleOutput[0].NoSuchEvent
RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
}
+
+test_SPF_Incorrect_V4 if {
+ # Fail if multiple records
+ PolicyId := GmailId3_1
+ Output := tests with input as {
+ "spf_records": [
+ {
+ "domain": "test.name",
+ "rdata": ["v=spf1 include:_spf.google.com -all", "v=spf1 include:_spf.google.com -all"]
+ }
+ ],
+ "domains": ["test.name"]
+ }
+
+ RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId]
+ count(RuleOutput) == 1
+ not RuleOutput[0].RequirementMet
+ not RuleOutput[0].NoSuchEvent
+ RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
+}
#--
diff --git a/scubagoggles/Testing/Unit/Rego/gmail/gmail04_test.rego b/scubagoggles/Testing/Unit/Rego/gmail/gmail04_test.rego
index d1ab62fe6..e7672fbf1 100644
--- a/scubagoggles/Testing/Unit/Rego/gmail/gmail04_test.rego
+++ b/scubagoggles/Testing/Unit/Rego/gmail/gmail04_test.rego
@@ -1,6 +1,7 @@
package gmail
import future.keywords
+MultipleWarning := "1 domain(s) have multiple DMARC records: test.name."
#
# GWS.GMAIL.4.1
#--
@@ -101,6 +102,30 @@ test_DMARC_Incorrect_V2 if {
RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
}
+test_DMARC_Incorrect_V3 if {
+ # Test DMARC when there are multiple dmarc records
+ PolicyId := GmailId4_1
+ Output := tests with input as {
+ "dmarc_records": [
+ {
+ "domain": "test.name",
+ "rdata": [
+ "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov",
+ "v=DMARC1; p=reject"
+ ]
+ }
+ ],
+ "domains": ["test.name"]
+ }
+
+ RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId]
+ count(RuleOutput) == 1
+ not RuleOutput[0].RequirementMet
+ not RuleOutput[0].NoSuchEvent
+ RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.",
+ MultipleWarning, DNSLink])
+}
+
#
# GWS.GMAIL.4.2
#--
@@ -201,6 +226,30 @@ test_DMARCMessageReject_Incorrect_V2 if {
RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
}
+test_DMARCMessageReject_Incorrect_V3 if {
+ # Test DMARC when there are multiple dmarc records
+ PolicyId := GmailId4_2
+ Output := tests with input as {
+ "dmarc_records": [
+ {
+ "domain": "test.name",
+ "rdata": [
+ "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov",
+ "v=DMARC1; p=reject"
+ ]
+ }
+ ],
+ "domains": ["test.name"]
+ }
+
+ RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId]
+ count(RuleOutput) == 1
+ not RuleOutput[0].RequirementMet
+ not RuleOutput[0].NoSuchEvent
+ RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.",
+ MultipleWarning, DNSLink])
+}
+
#
# GWS.GMAIL.4.3
#--
@@ -301,6 +350,31 @@ test_DMARCAggregateReports_Incorrect_V2 if {
RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
}
+test_DMARCAggregateReports_Incorrect_V3 if {
+ # Test DMARC when there are multiple dmarc records
+ PolicyId := GmailId4_3
+ Output := tests with input as {
+ "dmarc_records": [
+ {
+ "domain": "test.name",
+ "rdata": [
+ "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov",
+ "v=DMARC1; p=reject"
+ ]
+ }
+ ],
+ "domains": ["test.name"]
+ }
+
+ RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId]
+ count(RuleOutput) == 1
+ not RuleOutput[0].RequirementMet
+ not RuleOutput[0].NoSuchEvent
+ RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.",
+ MultipleWarning, DNSLink])
+}
+
+
#
# GWS.GMAIL.4.4
#--
@@ -400,4 +474,29 @@ test_DMARCAgencyPOC_Incorrect_V2 if {
not RuleOutput[0].NoSuchEvent
RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.", DNSLink])
}
+
+test_DMARCAgencyPOC_Incorrect_V3 if {
+ # Test DMARC when there are multiple dmarc records
+ PolicyId := GmailId4_4
+ Output := tests with input as {
+ "dmarc_records": [
+ {
+ "domain": "test.name",
+ "rdata": [
+ "v=DMARC1; p=reject; pct=100; rua=mailto:DMARC@hq.dhs.gov, mailto:reports@dmarc.cyber.dhs.gov",
+ "v=DMARC1; p=reject"
+ ]
+ }
+ ],
+ "domains": ["test.name"]
+ }
+
+ RuleOutput := [Result | some Result in Output; Result.PolicyId == PolicyId]
+ count(RuleOutput) == 1
+ not RuleOutput[0].RequirementMet
+ not RuleOutput[0].NoSuchEvent
+ RuleOutput[0].ReportDetails == concat(" ", ["1 of 1 agency domain(s) found in violation: test.name.",
+ MultipleWarning,
+ DNSLink])
+}
#--
diff --git a/scubagoggles/rego/Gmail.rego b/scubagoggles/rego/Gmail.rego
index 14a4d0e2b..d5e994d09 100644
--- a/scubagoggles/rego/Gmail.rego
+++ b/scubagoggles/rego/Gmail.rego
@@ -116,6 +116,8 @@ GmailId3_1 := utils.PolicyIdWithSuffix("GWS.GMAIL.3.1")
# Not applicable at OU or Group level
DomainsWithSpf contains SpfRecord.domain if {
some SpfRecord in input.spf_records
+ # Ensure that there's only 1 SPF record
+ count([Answer | some Answer in SpfRecord.rdata; startswith(Answer, "v=spf1")]) == 1
some Rdata in SpfRecord.rdata
startswith(Rdata, "v=spf1 ")
# Ensure that the policy either ends with "-all", "~all", or directs to a different SPF policy
@@ -155,8 +157,26 @@ GmailId4_1 := utils.PolicyIdWithSuffix("GWS.GMAIL.4.1")
# Not applicable at OU or Group level
DomainsWithDmarc contains DmarcRecord.domain if {
some DmarcRecord in input.dmarc_records
- some Rdata in DmarcRecord.rdata
- startswith(Rdata, "v=DMARC1;")
+ ValidAnswers := [Answer | some Answer in DmarcRecord.rdata; startswith(Answer, "v=DMARC1;")]
+ count(ValidAnswers) == 1
+}
+
+DomainsWithMultipleDmarc contains DmarcRecord.domain if {
+ some DmarcRecord in input.dmarc_records
+ ValidAnswers := [Answer | some Answer in DmarcRecord.rdata; startswith(Answer, "v=DMARC1;")]
+ count(ValidAnswers) > 1
+}
+
+MultiDmarcWarning := " " if { count(DomainsWithMultipleDmarc) == 0 }
+MultiDmarcWarning := Warning if {
+ count(DomainsWithMultipleDmarc) > 0
+ Warning := concat("", [
+ " ",
+ format_int(count(DomainsWithMultipleDmarc), 10),
+ " domain(s) have multiple DMARC records: ",
+ concat(", ", DomainsWithMultipleDmarc),
+ ". "
+ ])
}
tests contains {
@@ -167,7 +187,10 @@ tests contains {
"get_dmarc_records"
],
"Criticality": "Shall",
- "ReportDetails": concat(" ", [ReportDetailsArray(Status, DomainsWithoutDmarc, AllDomains), DNSLink]),
+ "ReportDetails": concat("", [
+ ReportDetailsArray(Status, DomainsWithoutDmarc, AllDomains),
+ MultiDmarcWarning,
+ DNSLink]),
"ActualValue": input.dmarc_records,
"RequirementMet": Status,
"NoSuchEvent": false
@@ -189,13 +212,18 @@ DomainsWithPreject contains DmarcRecord.domain if {
some DmarcRecord in input.dmarc_records
some Rdata in DmarcRecord.rdata
contains(Rdata, "p=reject;")
+ DmarcRecord.domain in DomainsWithDmarc
}
tests contains {
"PolicyId": GmailId4_2,
"Prerequisites": ["directory/v1/domains/list", "get_dmarc_records"],
"Criticality": "Shall",
- "ReportDetails": concat(" ", [ReportDetailsArray(Status, DomainsWithoutPreject, AllDomains), DNSLink]),
+ "ReportDetails": concat("", [
+ ReportDetailsArray(Status, DomainsWithoutPreject, AllDomains),
+ MultiDmarcWarning,
+ DNSLink
+ ]),
"ActualValue": input.dmarc_records,
"RequirementMet": Status,
"NoSuchEvent": false
@@ -217,13 +245,18 @@ DomainsWithDHSContact contains DmarcRecord.domain if {
some DmarcRecord in input.dmarc_records
some Rdata in DmarcRecord.rdata
contains(Rdata, "mailto:reports@dmarc.cyber.dhs.gov")
+ DmarcRecord.domain in DomainsWithDmarc
}
tests contains {
"PolicyId": GmailId4_3,
"Prerequisites": ["directory/v1/domains/list", "get_dmarc_records"],
"Criticality": "Shall",
- "ReportDetails": concat(" ", [ReportDetailsArray(Status, DomainsWithoutDHSContact, AllDomains), DNSLink]),
+ "ReportDetails": concat("", [
+ ReportDetailsArray(Status, DomainsWithoutDHSContact, AllDomains),
+ MultiDmarcWarning,
+ DNSLink
+ ]),
"ActualValue": input.dmarc_records,
"RequirementMet": Status,
"NoSuchEvent": false
@@ -245,13 +278,18 @@ DomainsWithAgencyContact contains DmarcRecord.domain if {
some DmarcRecord in input.dmarc_records
some Rdata in DmarcRecord.rdata
count(split(Rdata, "@")) >= 3
+ DmarcRecord.domain in DomainsWithDmarc
}
tests contains {
"PolicyId": GmailId4_4,
"Prerequisites": ["directory/v1/domains/list", "get_dmarc_records"],
"Criticality": "Should",
- "ReportDetails": concat(" ", [ReportDetailsArray(Status, DomainsWithoutAgencyContact, AllDomains), DNSLink]),
+ "ReportDetails": concat("", [
+ ReportDetailsArray(Status, DomainsWithoutAgencyContact, AllDomains),
+ MultiDmarcWarning,
+ DNSLink
+ ]),
"ActualValue": input.dmarc_records,
"RequirementMet": Status,
"NoSuchEvent": false
diff --git a/scubagoggles/reporter/reporter.py b/scubagoggles/reporter/reporter.py
index a3f1a1e8f..378904f51 100644
--- a/scubagoggles/reporter/reporter.py
+++ b/scubagoggles/reporter/reporter.py
@@ -875,7 +875,7 @@ def _build_report_html(self,
"Query Name": qname,
"Query Method": query['query_method'],
"Summary": query['query_result'],
- "Answers": '\n'.join(answers)
+ "Answers": '
'.join(answers)
})
log_table = self.create_html_table(logs)
log_table = log_table.replace("