Skip to content

Commit 744cb02

Browse files
Add a parser for OpenSCAP vulnerability/compliance reports in JSON format (#1522)
1 parent 8bebec2 commit 744cb02

9 files changed

Lines changed: 502 additions & 1 deletion

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package edu.hm.hafner.analysis.parser;
2+
3+
import java.io.Serial;
4+
import java.util.Locale;
5+
6+
import org.json.JSONArray;
7+
import org.json.JSONObject;
8+
import org.apache.commons.lang3.StringUtils;
9+
10+
import edu.hm.hafner.analysis.IssueBuilder;
11+
import edu.hm.hafner.analysis.Issue;
12+
import edu.hm.hafner.analysis.Report;
13+
14+
/**
15+
* Parser for OpenSCAP vulnerability/compliance reports in JSON format.
16+
*
17+
* @author Akash Manna
18+
* @see <a href="https://github.com/OpenSCAP/openscap">OpenSCAP on GitHub</a>
19+
* @see <a href="https://www.open-scap.org/resources/documentation/">OpenSCAP Documentation</a>
20+
*/
21+
public class OpenScapParser extends JsonIssueParser {
22+
@Serial
23+
private static final long serialVersionUID = -1234567890123456789L;
24+
25+
private static final String TEST_RESULTS_TAG = "test_results";
26+
private static final String RULES_TAG = "rules";
27+
private static final String RESULT_TAG = "result";
28+
private static final String RULE_TAG = "rule";
29+
private static final String RULE_ID_TAG = "id";
30+
private static final String TITLE_TAG = "title";
31+
private static final String DESCRIPTION_TAG = "description";
32+
private static final String SEVERITY_TAG = "severity";
33+
private static final String EVIDENCE_TAG = "evidence";
34+
private static final String FILE_TAG = "file";
35+
36+
@Override
37+
protected void parseJsonObject(final Report report, final JSONObject jsonReport, final IssueBuilder issueBuilder) {
38+
JSONArray testResults = jsonReport.optJSONArray(TEST_RESULTS_TAG);
39+
if (testResults != null) {
40+
parseTestResults(report, testResults, issueBuilder);
41+
}
42+
43+
JSONArray rules = jsonReport.optJSONArray(RULES_TAG);
44+
if (rules != null) {
45+
parseRules(report, rules, issueBuilder);
46+
}
47+
}
48+
49+
private void parseTestResults(final Report report, final JSONArray testResults, final IssueBuilder issueBuilder) {
50+
for (int i = 0; i < testResults.length(); i++) {
51+
JSONObject result = testResults.getJSONObject(i);
52+
53+
String resultStatus = result.optString(RESULT_TAG, "");
54+
if (shouldReportResult(resultStatus)) {
55+
report.add(createIssueFromTestResult(result, issueBuilder));
56+
}
57+
}
58+
}
59+
60+
private void parseRules(final Report report, final JSONArray rules, final IssueBuilder issueBuilder) {
61+
for (int i = 0; i < rules.length(); i++) {
62+
JSONObject rule = rules.getJSONObject(i);
63+
64+
String resultStatus = rule.optString(RESULT_TAG, "");
65+
if (shouldReportResult(resultStatus)) {
66+
report.add(createIssueFromRule(rule, issueBuilder));
67+
}
68+
}
69+
}
70+
71+
private boolean shouldReportResult(final String resultStatus) {
72+
String normalized = StringUtils.lowerCase(resultStatus, Locale.ENGLISH);
73+
return "fail".equals(normalized) || "error".equals(normalized);
74+
}
75+
76+
private Issue createIssueFromTestResult(final JSONObject testResult, final IssueBuilder issueBuilder) {
77+
JSONObject rule = testResult.optJSONObject(RULE_TAG);
78+
79+
String ruleId = "-";
80+
String title = "Unknown";
81+
String description = "";
82+
83+
if (rule != null) {
84+
ruleId = rule.optString(RULE_ID_TAG, "-");
85+
title = rule.optString(TITLE_TAG, "Unknown");
86+
description = rule.optString(DESCRIPTION_TAG, "");
87+
}
88+
89+
String severity = testResult.optString(SEVERITY_TAG, "medium");
90+
String evidence = testResult.optString(EVIDENCE_TAG, "");
91+
String file = testResult.optString(FILE_TAG, "-");
92+
String resultStatus = testResult.optString(RESULT_TAG, "");
93+
94+
String message = title;
95+
if (!evidence.isEmpty()) {
96+
message = title + " - " + evidence;
97+
}
98+
99+
String descriptionText = description;
100+
if (!evidence.isEmpty() && description.isEmpty()) {
101+
descriptionText = evidence;
102+
}
103+
else if (!evidence.isEmpty()) {
104+
descriptionText = description + "\n\nEvidence: " + evidence;
105+
}
106+
107+
return issueBuilder
108+
.setFileName(file)
109+
.setType(ruleId)
110+
.setMessage(message)
111+
.setDescription(descriptionText)
112+
.setCategory(resultStatus)
113+
.guessSeverity(severity)
114+
.buildAndClean();
115+
}
116+
117+
private Issue createIssueFromRule(final JSONObject rule, final IssueBuilder issueBuilder) {
118+
String ruleId = rule.optString(RULE_ID_TAG, "-");
119+
String title = rule.optString(TITLE_TAG, "Unknown");
120+
String description = rule.optString(DESCRIPTION_TAG, "");
121+
String severity = rule.optString(SEVERITY_TAG, "medium");
122+
String file = rule.optString(FILE_TAG, "-");
123+
String resultStatus = rule.optString(RESULT_TAG, "");
124+
125+
return issueBuilder
126+
.setFileName(file)
127+
.setType(ruleId)
128+
.setMessage(title)
129+
.setDescription(description)
130+
.setCategory(resultStatus)
131+
.guessSeverity(severity)
132+
.buildAndClean();
133+
}
134+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package edu.hm.hafner.analysis.registry;
2+
3+
import edu.hm.hafner.analysis.IssueParser;
4+
import edu.hm.hafner.analysis.Report.IssueType;
5+
import edu.hm.hafner.analysis.parser.OpenScapParser;
6+
7+
/**
8+
* A descriptor for OpenSCAP compliance and vulnerability scanner.
9+
*
10+
* @author Akash Manna
11+
*/
12+
class OpenScapDescriptor extends ParserDescriptor {
13+
private static final String ID = "openscap";
14+
private static final String NAME = "OpenSCAP";
15+
16+
OpenScapDescriptor() {
17+
super(ID, NAME);
18+
}
19+
20+
@Override
21+
public IssueType getType() {
22+
return IssueType.WARNING;
23+
}
24+
25+
@Override
26+
public String getIconUrl() {
27+
return "https://github.com/OpenSCAP/openscap/blob/main/docs/manual/images/vertical-logo.png";
28+
}
29+
30+
@Override
31+
public IssueParser create(final Option... options) {
32+
return new OpenScapParser();
33+
}
34+
35+
@Override
36+
public String getPattern() {
37+
return "**/openscap-report.json";
38+
}
39+
40+
@Override
41+
public String getHelp() {
42+
return "Use commandline <code>oscap scan --results-arf results.xml --report report.html</code> "
43+
+ "to generate reports. Convert ARF to JSON for automated parsing.<br/>"
44+
+ "See <a href='https://github.com/OpenSCAP/openscap'>OpenSCAP on GitHub</a> for usage details.";
45+
}
46+
47+
@Override
48+
public String getUrl() {
49+
return "https://github.com/OpenSCAP/openscap";
50+
}
51+
}

src/main/java/edu/hm/hafner/analysis/registry/ParserRegistry.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public class ParserRegistry {
143143
new NixDescriptor(),
144144
new NpmAuditDescriptor(),
145145
new OeLintAdvDescriptor(),
146+
new OpenScapDescriptor(),
146147
new OtDockerLintDescriptor(),
147148
new OwaspDependencyCheckDescriptor(),
148149
new PcLintDescriptor(),
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package edu.hm.hafner.analysis.parser;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import edu.hm.hafner.analysis.Issue;
6+
import edu.hm.hafner.analysis.IssueParser;
7+
import edu.hm.hafner.analysis.Report;
8+
import edu.hm.hafner.analysis.Severity;
9+
import edu.hm.hafner.analysis.assertions.SoftAssertions;
10+
import edu.hm.hafner.analysis.registry.AbstractParserTest;
11+
import edu.hm.hafner.analysis.registry.ParserRegistry;
12+
import edu.hm.hafner.analysis.Report.IssueType;
13+
14+
import static edu.hm.hafner.analysis.assertions.Assertions.*;
15+
16+
/**
17+
* Tests the class {@link OpenScapParser}.
18+
*
19+
* @author Akash Manna
20+
*/
21+
class OpenScapParserTest extends AbstractParserTest {
22+
OpenScapParserTest() {
23+
super("openscap-report.json");
24+
}
25+
26+
@Override
27+
protected IssueParser createParser() {
28+
return new OpenScapParser();
29+
}
30+
31+
@Override
32+
protected void assertThatIssuesArePresent(final Report report, final SoftAssertions softly) {
33+
softly.assertThat(report).hasSize(3).hasDuplicatesSize(0);
34+
35+
softly.assertThat(report.get(0))
36+
.hasFileName("config/login.defs")
37+
.hasSeverity(Severity.WARNING_HIGH)
38+
.hasType("xccdf_org.ssgproject.content_rule_accounts_umask_etc_login_defs")
39+
.hasMessage("Set Default umask for Users - umask setting is not restrictive enough")
40+
.hasCategory("fail");
41+
42+
softly.assertThat(report.get(0).getDescription())
43+
.contains("The umask controls the default permissions of newly created files")
44+
.contains("Evidence: umask setting is not restrictive enough");
45+
46+
softly.assertThat(report.get(1))
47+
.hasFileName("config/selinux")
48+
.hasSeverity(Severity.WARNING_NORMAL)
49+
.hasType("xccdf_org.ssgproject.content_rule_selinux_policy")
50+
.hasMessage("Ensure SELinux Policy is Configured - SELinux is not enabled")
51+
.hasCategory("fail");
52+
53+
softly.assertThat(report.get(1).getDescription())
54+
.contains("SELinux provides mandatory access control policies")
55+
.contains("Evidence: SELinux is not enabled");
56+
57+
softly.assertThat(report.get(2))
58+
.hasFileName("config/sshd_config")
59+
.hasSeverity(Severity.ERROR)
60+
.hasType("xccdf_org.ssgproject.content_rule_ssh_password_authentication")
61+
.hasMessage("Disable SSH Password Authentication - PasswordAuthentication is enabled in SSH configuration")
62+
.hasCategory("fail");
63+
64+
softly.assertThat(report.get(2).getDescription())
65+
.contains("SSH password authentication should be disabled")
66+
.contains("Evidence: PasswordAuthentication is enabled in SSH configuration");
67+
}
68+
69+
@Test
70+
void shouldParseEdgeCases() {
71+
var report = parse("openscap-report-edge-cases.json");
72+
73+
assertThat(report).hasSize(2).hasDuplicatesSize(0);
74+
75+
assertThat(report.get(0))
76+
.hasFileName("config/firewall")
77+
.hasSeverity(Severity.WARNING_HIGH)
78+
.hasType("xccdf_org.ssgproject.content_rule_firewall_enabled")
79+
.hasMessage("Firewall Service Enabled")
80+
.hasCategory("fail");
81+
82+
assertThat(report.get(0).getDescription())
83+
.contains("The firewall service should be enabled to protect the system");
84+
85+
assertThat(report.get(1))
86+
.hasFileName("config/audit")
87+
.hasSeverity(Severity.WARNING_NORMAL)
88+
.hasType("xccdf_org.ssgproject.content_rule_audit_enabled")
89+
.hasMessage("Audit Daemon Enabled")
90+
.hasCategory("error");
91+
92+
assertThat(report.get(1).getDescription())
93+
.contains("The audit daemon should be enabled to log system activities");
94+
}
95+
96+
@Test
97+
void shouldNotReportPassResults() {
98+
var report = parse("openscap-report.json");
99+
assertThat(report.get())
100+
.map(Issue::getCategory)
101+
.doesNotContain("pass", "notapplicable");
102+
}
103+
104+
@Test
105+
void shouldHandleNullRuleObject() {
106+
var report = parse("openscap-report-null-handling.json");
107+
108+
assertThat(report).hasSize(3).hasDuplicatesSize(0);
109+
110+
assertThat(report.get(0))
111+
.hasFileName("config/test.conf")
112+
.hasSeverity(Severity.WARNING_HIGH)
113+
.hasType("-")
114+
.hasMessage("Unknown - Test without rule object")
115+
.hasCategory("fail");
116+
117+
assertThat(report.get(1))
118+
.hasFileName("config/test2.conf")
119+
.hasSeverity(Severity.ERROR)
120+
.hasType("test-rule-1")
121+
.hasMessage("Test Rule With Description")
122+
.hasCategory("fail");
123+
124+
assertThat(report.get(1).getDescription())
125+
.contains("Detailed description of the issue");
126+
127+
assertThat(report.get(2))
128+
.hasFileName("config/test3.conf")
129+
.hasSeverity(Severity.WARNING_LOW)
130+
.hasType("test-rule-2")
131+
.hasMessage("Test Rule Without Description")
132+
.hasCategory("error");
133+
}
134+
135+
@Test
136+
void shouldCombineEvidenceAndDescription() {
137+
var report = parse("openscap-report-null-handling.json");
138+
assertThat(report.get(0)).hasDescription("Test without rule object");
139+
assertThat(report.get(1)).hasDescription("Detailed description of the issue");
140+
}
141+
142+
@Test
143+
void shouldMapSeverityCorrectly() {
144+
var report = parse("openscap-report-null-handling.json");
145+
146+
assertThat(report.get(0)).hasSeverity(Severity.WARNING_HIGH);
147+
assertThat(report.get(1)).hasSeverity(Severity.ERROR);
148+
assertThat(report.get(2)).hasSeverity(Severity.WARNING_LOW);
149+
}
150+
151+
@Test
152+
void shouldFilterFailAndErrorResults() {
153+
var report = parse("openscap-report-null-handling.json");
154+
assertOnlyFailureCategories(report);
155+
}
156+
157+
@Test
158+
void shouldUseDefaultFileNameWhenMissing() {
159+
var report = parse("openscap-report-null-handling.json");
160+
assertThat(report.get())
161+
.map(Issue::getFileName)
162+
.allSatisfy(fileName -> assertThat(fileName).isNotNull().isNotEmpty());
163+
}
164+
165+
@Test
166+
void shouldParseMainReport() {
167+
assertOnlyFailureCategories(parse("openscap-report.json"));
168+
}
169+
170+
@Test
171+
void shouldParseEdgeCaseReport() {
172+
assertOnlyFailureCategories(parse("openscap-report-edge-cases.json"));
173+
}
174+
175+
@Test
176+
void shouldProvideDescriptorMetadata() {
177+
var descriptor = new ParserRegistry().get("openscap");
178+
179+
assertThat(descriptor.getPattern()).isEqualTo("**/openscap-report.json");
180+
assertThat(descriptor.getHelp()).contains("oscap scan --results-arf results.xml --report report.html");
181+
assertThat(descriptor.getUrl()).isEqualTo("https://github.com/OpenSCAP/openscap");
182+
assertThat(descriptor.getIconUrl()).isEqualTo("https://github.com/OpenSCAP/openscap/blob/main/docs/manual/images/vertical-logo.png");
183+
assertThat(descriptor.getType()).isEqualTo(IssueType.WARNING);
184+
assertThat(descriptor.hasHelp()).isTrue();
185+
assertThat(descriptor.hasUrl()).isTrue();
186+
}
187+
188+
private void assertOnlyFailureCategories(final Report report) {
189+
assertThat(report.get())
190+
.map(Issue::getCategory)
191+
.allSatisfy(category -> assertThat(category).isIn("fail", "error"));
192+
}
193+
}

src/test/java/edu/hm/hafner/analysis/registry/ParserRegistryTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
class ParserRegistryTest extends ResourceTest {
2323
// Note for parser developers: if you add a new parser,
2424
// please check if you are using the correct type and increment the corresponding count
25-
private static final long WARNING_PARSERS_COUNT = 152L;
25+
private static final long WARNING_PARSERS_COUNT = 153L;
2626
private static final long BUG_PARSERS_COUNT = 3L;
2727
private static final long VULNERABILITY_PARSERS_COUNT = 17L;
2828
private static final long DUPLICATION_PARSERS_COUNT = 3L;

0 commit comments

Comments
 (0)