Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions fcli-core/fcli-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {

// Inter-project dependencies
val refs = listOf(
"fcliCommonRef","fcliActionRef","fcliAviatorRef","fcliConfigRef","fcliFoDRef","fcliSSCRef","fcliSCSastRef","fcliSCDastRef","fcliToolRef","fcliLicenseRef","fcliUtilRef"
"fcliCommonRef","fcliActionRef","fcliAviatorRef","fcliConfigRef","fcliFoDRef","fcliSSCRef","fcliSCSastRef","fcliSCDastRef","fcliToolRef","fcliLicenseRef","fcliUtilRef","fcliFPRRef"
)
references@ for (r in refs) {
val p = project.findProperty(r) as String? ?: continue@references
Expand Down Expand Up @@ -128,4 +128,4 @@ tasks.register<Copy>("dist") {
into(rootProject.layout.buildDirectory.dir("dist/release-assets"))
inputs.file(layout.buildDirectory.file("libs/fcli.jar"))
outputs.dir(rootProject.layout.buildDirectory.dir("dist/release-assets"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.fortify.cli.common.util.DisableTest.TestType;
import com.fortify.cli.config._main.cli.cmd.ConfigCommands;
import com.fortify.cli.fod._main.cli.cmd.FoDCommands;
import com.fortify.cli.fpr._main.cli.cmd.FPRCommands;
import com.fortify.cli.generic_action._main.cli.cmd.GenericActionCommands;
import com.fortify.cli.license._main.cli.cmd.LicenseCommands;
import com.fortify.cli.sc_dast._main.cli.cmd.SCDastCommands;
Expand Down Expand Up @@ -54,6 +55,7 @@
SSCCommands.class,
ToolCommands.class,
LicenseCommands.class,
FPRCommands.class,
UtilCommands.class
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class AuditIssue {
private int revision;
@Builder.Default private Map<String, String> tags = new HashMap<>();
@Builder.Default private List<Comment> threadedComments = new ArrayList<>();
@Builder.Default private List<TagHistoryEntry> tagHistory = new ArrayList<>();
private String assignedUser;

public void addTag(String tagId, String tagValue) {
if (tagId != null) {
Expand All @@ -52,4 +54,14 @@ public static class Comment {
private String username;
private String timestamp;
}
}

@Getter
@Builder
@Reflectable
public static class TagHistoryEntry {
private String tagId;
private String tagValue;
private String editTime;
private String username;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package com.fortify.cli.aviator.fpr.processor;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
Expand Down Expand Up @@ -93,20 +94,21 @@ public Map<String, AuditIssue> processAuditXML() throws AviatorTechnicalExceptio
if (!Files.exists(auditPath)) {
logger.debug("audit.xml not found. Creating a default audit.xml.");
auditDoc = createDefaultAuditXml();
}

DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
} else {

try (InputStream auditStream = Files.newInputStream(auditPath)) {
auditDoc = builder.parse(auditStream);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();

try (InputStream auditStream = Files.newInputStream(auditPath)) {
auditDoc = builder.parse(auditStream);
}
}
NodeList issueNodes = auditDoc.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Issue");
for (int i = 0; i < issueNodes.getLength(); i++) {
Expand Down Expand Up @@ -183,6 +185,9 @@ private AuditIssue processAuditIssue(Element issueElement) {
AuditIssue.AuditIssueBuilder auditIssueBuilder = AuditIssue.builder();

auditIssueBuilder.instanceId(issueElement.getAttribute("instanceId"));
if (issueElement.hasAttribute("assignedUser")) {
auditIssueBuilder.assignedUser(issueElement.getAttribute("assignedUser"));
}
auditIssueBuilder.suppressed(Boolean.parseBoolean(issueElement.getAttribute("suppressed")));

String revisionStr = issueElement.getAttribute("revision");
Expand Down Expand Up @@ -211,6 +216,31 @@ private AuditIssue processAuditIssue(Element issueElement) {
}
auditIssueBuilder.threadedComments(threadedComments);

List<AuditIssue.TagHistoryEntry> tagHistoryEntries = new ArrayList<>();
NodeList clientAuditTrailNodes = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "ClientAuditTrail");
if (clientAuditTrailNodes.getLength() > 0) {
Element catElement = (Element) clientAuditTrailNodes.item(0);
NodeList thNodes = catElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "TagHistory");
for (int j = 0; j < thNodes.getLength(); j++) {
Element thElement = (Element) thNodes.item(j);
String thTagId = "";
String thTagValue = "";
NodeList thTagNodes = thElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Tag");
if (thTagNodes.getLength() > 0) {
Element thTagElement = (Element) thTagNodes.item(0);
thTagId = thTagElement.getAttribute("id");
thTagValue = Optional.ofNullable(getTagValue(thTagElement)).orElse("");
}
tagHistoryEntries.add(AuditIssue.TagHistoryEntry.builder()
.tagId(thTagId)
.tagValue(thTagValue)
.editTime(Optional.ofNullable(getFirstElementContentNS(thElement, "EditTime")).orElse(""))
.username(Optional.ofNullable(getFirstElementContentNS(thElement, "Username")).orElse(""))
.build());
}
}
auditIssueBuilder.tagHistory(tagHistoryEntries);

return auditIssueBuilder.build();
}

Expand Down Expand Up @@ -247,6 +277,152 @@ public void updateIssueTag(AuditIssue auditIssue, String tagId, String tagValue)
updateOrAddTag(issueElement, tagId, tagValue);
}

/**
* Performs a simple audit on a single issue: sets the given tag value,
* optionally adds a comment, and optionally suppresses the issue.
* The tag and comment are recorded in ClientAuditTrail for history.
*/
public boolean auditIssue(String instanceId, String tagId, String tagValue,
String comment, String username, boolean suppress) {
return auditIssueMulti(instanceId, java.util.Map.of(tagId, tagValue), comment, username, suppress, null);
}

/**
* Backwards-compatible overload without an assigned user.
*/
public boolean auditIssueMulti(String instanceId, java.util.Map<String, String> tagIdToValue,
String comment, String username, boolean suppress) {
return auditIssueMulti(instanceId, tagIdToValue, comment, username, suppress, null);
}

/**
* Audits a single issue, applying multiple tag changes atomically: bumps revision once,
* writes only the tags whose value actually changes (and a TagHistory entry for each),
* appends an optional comment once, optionally suppresses the issue, and optionally
* assigns the issue to a user (stored as the {@code assignedUser} attribute on the
* {@code <Issue>} element). Returns true if anything changed, false if the call was a no-op.
*/
public boolean auditIssueMulti(String instanceId, java.util.Map<String, String> tagIdToValue,
String comment, String username, boolean suppress,
String assignedUser) {
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("username must be provided");
}
if (tagIdToValue == null) { tagIdToValue = java.util.Map.of(); }

Element issueElement = findIssueElement(instanceId);
boolean issueCreated = false;
if (issueElement == null) {
issueElement = createSimpleIssueElement(instanceId);
issueCreated = true;
}

java.util.Map<String, String> changedTags = new java.util.LinkedHashMap<>();
for (var entry : tagIdToValue.entrySet()) {
String tagId = entry.getKey();
String tagValue = entry.getValue();
if (tagId == null || tagId.isBlank() || tagValue == null) { continue; }
String currentTagValue = getCurrentTagValue(issueElement, tagId);
if (!java.util.Objects.equals(tagValue, currentTagValue)) {
changedTags.put(tagId, tagValue);
}
}

boolean suppressChanged = suppress && !"true".equalsIgnoreCase(issueElement.getAttribute("suppressed"));
boolean commentAdded = comment != null && !comment.isBlank();

boolean assignChanged = false;
if (assignedUser != null) {
String currentAssigned = issueElement.hasAttribute("assignedUser")
? issueElement.getAttribute("assignedUser") : "";
// Empty string clears the assignment.
if (!assignedUser.equals(currentAssigned)) {
assignChanged = true;
}
}

if (changedTags.isEmpty() && !suppressChanged && !commentAdded && !assignChanged) {
return false;
}

int revision = Optional.ofNullable(issueElement.getAttribute("revision"))
.filter(s -> !s.isEmpty())
.map(Integer::parseInt)
.orElse(0);
issueElement.setAttribute("revision", String.valueOf(revision + 1));

if (!changedTags.isEmpty()) {
Element clientAuditTrail = getClientAuditTrailElement(issueElement);
for (var ct : changedTags.entrySet()) {
updateOrAddTag(issueElement, ct.getKey(), ct.getValue());
addTagHistory(clientAuditTrail, ct.getKey(), ct.getValue(), username);
}
}
if (commentAdded) {
addCommentToIssueElement(issueElement, comment, username);
}
if (suppressChanged) {
issueElement.setAttribute("suppressed", "true");
}
if (assignChanged) {
if (assignedUser.isEmpty()) {
issueElement.removeAttribute("assignedUser");
} else {
issueElement.setAttribute("assignedUser", assignedUser);
}
}
return !changedTags.isEmpty() || suppressChanged || commentAdded || assignChanged || issueCreated;
}

private String getCurrentTagValue(Element issueElement, String tagId) {
NodeList tagNodes = issueElement.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Tag");
for (int i = 0; i < tagNodes.getLength(); i++) {
Element tag = (Element) tagNodes.item(i);
if (tag.getAttribute("id").equalsIgnoreCase(tagId)) {
NodeList valueNodes = tag.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "Value");
if (valueNodes.getLength() > 0) {
return valueNodes.item(0).getTextContent();
}
}
}
return null;
}

private Element createSimpleIssueElement(String instanceId) {
Element issueList = (Element) auditDoc.getElementsByTagNameNS(AUDIT_NAMESPACE_URI, "IssueList").item(0);
if (issueList == null) {
issueList = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "IssueList");
auditDoc.getDocumentElement().appendChild(issueList);
}
Element newIssue = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "Issue");
newIssue.setAttribute("instanceId", instanceId);
newIssue.setAttribute("revision", "0");
newIssue.setAttribute("suppressed", "false");
issueList.appendChild(newIssue);
return newIssue;
}

/**
* Writes the in-memory audit.xml back into the FPR file.
*/
public void saveAuditXml() {
// Serialize to memory first; only write to FPR if serialization succeeds,
// to avoid leaving a corrupted/truncated audit.xml inside the FPR file.
byte[] serialized;
try {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
transformDomToStream(auditDoc, buffer);
serialized = buffer.toByteArray();
} catch (Exception e) {
throw new AviatorTechnicalException("Failed to serialize audit.xml", e);
}
try (OutputStream os = Files.newOutputStream(fprHandle.getPath("/audit.xml"))) {
os.write(serialized);
} catch (IOException e) {
throw new AviatorTechnicalException("Failed to write audit.xml back into the FPR file", e);
}
}

private Map<String, String> updateAuditXml(Map<String, AuditResponse> auditResponses, TagMappingConfig tagMappingConfig) throws AviatorTechnicalException {
Map<String, String> remediationCommentTimestamps = new HashMap<>();
for (Map.Entry<String, AuditResponse> entry : auditResponses.entrySet()) {
Expand Down Expand Up @@ -391,6 +567,10 @@ private Element getClientAuditTrailElement(Element issueElement) {
}

private void addTagHistory(Element clientAuditTrail, String tagId, String tagValue) {
addTagHistory(clientAuditTrail, tagId, tagValue, Constants.USER_NAME);
}

private void addTagHistory(Element clientAuditTrail, String tagId, String tagValue, String username) {
Element tagHistory = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "TagHistory");

Element tag = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "Tag");
Expand All @@ -405,9 +585,9 @@ private void addTagHistory(Element clientAuditTrail, String tagId, String tagVal
editTime.setTextContent(dateFormat.format(new Date()));
tagHistory.appendChild(editTime);

Element username = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "Username");
username.setTextContent("Fortify Aviator");
tagHistory.appendChild(username);
Element usernameElement = auditDoc.createElementNS(AUDIT_NAMESPACE_URI, "Username");
usernameElement.setTextContent(username);
tagHistory.appendChild(usernameElement);

clientAuditTrail.appendChild(tagHistory);
}
Expand Down
Loading
Loading