////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2009-2013 Denim Group, Ltd.
//
// The contents of this file are subject to the Mozilla Public License
// Version 2.0 (the "License"); you may not use this file except in
// compliance with the License. You may obtain a copy of the License at
// http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS"
// basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
// License for the specific language governing rights and limitations
// under the License.
//
// The Original Code is ThreadFix.
//
// The Initial Developer of the Original Code is Denim Group, Ltd.
// Portions created by Denim Group, Ltd. are Copyright (C)
// Denim Group, Ltd. All Rights Reserved.
//
// Contributor(s): Denim Group, Ltd.
//
////////////////////////////////////////////////////////////////////////
package com.denimgroup.threadfix.service.waf;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import com.denimgroup.threadfix.data.dao.WafRuleDao;
import com.denimgroup.threadfix.data.dao.WafRuleDirectiveDao;
import com.denimgroup.threadfix.data.entities.Finding;
import com.denimgroup.threadfix.data.entities.GenericVulnerability;
import com.denimgroup.threadfix.data.entities.SurfaceLocation;
import com.denimgroup.threadfix.data.entities.Vulnerability;
import com.denimgroup.threadfix.data.entities.WafRule;
import com.denimgroup.threadfix.data.entities.WafRuleDirective;
/**
* This class uses a different system for generating rules because
* BIG-IP accepts a large policy XML that is a combination of rules instead of
* standalone rules like mod_security or Snort do.
* <br><br>
*
* @author mcollins
*
*/
public class BigIPASMGenerator extends RealTimeProtectionGenerator {
//TODO change structure of getStart / getEnd here and in other classes
public BigIPASMGenerator(WafRuleDao wafRuleDao, WafRuleDirectiveDao wafRuleDirectiveDao) {
this.wafRuleDao = wafRuleDao;
this.wafRuleDirectiveDao = wafRuleDirectiveDao;
defaultDirective = "transparent";
}
/**
* This map is used to skip a lot of if/then statements when selecting a set of attack signatures.
*/
private static Map<String, String[]> vulnTypeSignatureMap = new HashMap<String, String[]>();
static {
vulnTypeSignatureMap.put(GenericVulnerability.CWE_CROSS_SITE_SCRIPTING, BigIPStrings.SIGS_XSS);
vulnTypeSignatureMap.put(GenericVulnerability.CWE_PATH_TRAVERSAL, BigIPStrings.SIGS_PATH_TRAVERSAL);
vulnTypeSignatureMap.put(GenericVulnerability.CWE_SQL_INJECTION, BigIPStrings.SIGS_SQLI);
vulnTypeSignatureMap.put(GenericVulnerability.CWE_XPATH_INJECTION, BigIPStrings.SIGS_XPATH);
vulnTypeSignatureMap.put(GenericVulnerability.CWE_BLIND_XPATH_INJECTION, BigIPStrings.SIGS_XPATH);
vulnTypeSignatureMap.put(GenericVulnerability.CWE_FILE_UPLOAD, BigIPStrings.SIGS_FILE_UPLOAD);
}
private static Map<String, String> vulnTypeSigSetMap = new HashMap<String, String>();
static {
vulnTypeSigSetMap.put(GenericVulnerability.CWE_CROSS_SITE_SCRIPTING, "299999994");
vulnTypeSigSetMap.put(GenericVulnerability.CWE_PATH_TRAVERSAL, "299999990");
vulnTypeSigSetMap.put(GenericVulnerability.CWE_SQL_INJECTION, "299999994");
vulnTypeSigSetMap.put(GenericVulnerability.CWE_XPATH_INJECTION, "299999989");
vulnTypeSigSetMap.put(GenericVulnerability.CWE_BLIND_XPATH_INJECTION, "299999989");
}
@Override
public String[] getSupportedVulnerabilityTypes() {
return new String[] {
GenericVulnerability.CWE_CROSS_SITE_SCRIPTING,
GenericVulnerability.CWE_PATH_TRAVERSAL,
GenericVulnerability.CWE_SQL_INJECTION,
GenericVulnerability.CWE_CROSS_SITE_REQUEST_FORGERY,
GenericVulnerability.CWE_XPATH_INJECTION,
GenericVulnerability.CWE_BLIND_XPATH_INJECTION,
GenericVulnerability.CWE_INFORMATION_EXPOSURE,
GenericVulnerability.CWE_PRIVACY_VIOLATION,
GenericVulnerability.CWE_FILE_UPLOAD,
GenericVulnerability.CWE_GENERIC_INJECTION,
GenericVulnerability.CWE_DEBUG_CODE
};
}
@Override
protected WafRule makeRule(Integer currentId, Vulnerability vulnerability, WafRuleDirective directive) {
if (currentId == null || vulnerability == null
|| vulnerability.getSurfaceLocation() == null
|| vulnerability.getGenericVulnerability() == null
|| vulnerability.getGenericVulnerability().getName() == null) {
return null;
}
SurfaceLocation surfaceLocation = vulnerability.getSurfaceLocation();
String vulnType = vulnerability.getGenericVulnerability().getName();
// Check if the vuln is supported
if (!stringInList(vulnType, getSupportedVulnerabilityTypes())) {
return null;
}
String parameter = surfaceLocation.getParameter();
String path = surfaceLocation.getPath();
WafRule rule = new WafRule();
rule.setIsNormalRule(false);
rule.setWafRuleDirective(directive);
rule.setNativeId(currentId.toString());
//CSRF is handled on a by-url basis in its own tag
if (GenericVulnerability.CWE_CROSS_SITE_REQUEST_FORGERY.equals(vulnType)) {
rule.setVulnerabilityDesc("CSRF");
rule.setRule("<csrf_urls>" + path + "</csrf_urls>");
return rule;
}
// Possibly turn on Response Scrubbing if CCN or SSN might be present
if (GenericVulnerability.CWE_INFORMATION_EXPOSURE.equals(vulnType) ||
GenericVulnerability.CWE_PRIVACY_VIOLATION.equals(vulnType)) {
for (Finding finding : vulnerability.getFindings()) {
if (finding != null && finding.getChannelVulnerability() != null &&
finding.getChannelVulnerability().getName()!= null &&
(finding.getChannelVulnerability().getName().contains("Credit Card") ||
finding.getChannelVulnerability().getName().contains("Social Security"))) {
rule.setRule("Response Scrubbing");
return rule;
}
}
return null;
}
// Possibly turn on Illegal methods
// TODO improve detection of these vulns
if (GenericVulnerability.CWE_GENERIC_INJECTION.equals(vulnType) ||
GenericVulnerability.CWE_DEBUG_CODE.equals(vulnType)) {
for (Finding finding : vulnerability.getFindings()) {
if (finding != null && finding.getChannelVulnerability() != null &&
finding.getChannelVulnerability().getName()!= null &&
finding.getChannelVulnerability().getName().contains("HTTP Method")) {
rule.setRule("Illegal Method");
return rule;
}
}
return null;
}
// The general case: set the path, parameter, and type
if (path != null && (parameter != null || GenericVulnerability.CWE_FILE_UPLOAD.equals(vulnType))) {
rule.setParameter(parameter);
rule.setPath(path);
rule.setRule("BIG-IP");
rule.setVulnerabilityDesc(vulnType);
return rule;
}
return null;
}
/**
* Generate the first part of the policy.
*
* The general strategy is to compose a non-repeating structure of URL/param pairs,
* keeping track of corresponding attack signatures and combining them if necessary,
* then expanding that into the policy string using string templates.
* <br/><br/>
* Non-attack signature related rules are handled separately, adding to the length and
* complexity of this method.
*
* TODO split into smaller methods
* @param rules
* @return
*/
public static String getStart(List<WafRule> rules) {
if (rules == null || rules.size() == 0) {
return null;
}
String directive = null;
StringBuilder csrfStrings = new StringBuilder();
List<String> csrfStringList = new ArrayList<String>();
StringBuilder ruleTextBuilder = new StringBuilder();
// First keys are paths
// Second keys are parameters
// Set<String> values are the signatures
Map<String, Map<String, Set<String>>> ruleMap =
new HashMap<String, Map<String,Set<String>>>();
Map<String, String> dateMap = new HashMap<String, String>();
boolean directoryTraversalsOn = false;
boolean responseScrubbingOn = false;
boolean illegalMethodOn = false;
// Construct the map<string, map<string, set<string>>> structure
for (WafRule rule : rules) {
if (rule == null || rule.getRule() == null) {
continue;
}
// get values from rule
String path = rule.getPath();
String parameter = rule.getParameter();
String[] attackSignatures = vulnTypeSignatureMap.get(rule.getVulnerabilityDesc());
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = null;
if (rule.getCreatedDate() != null) {
date = dateFormatter.format(rule.getCreatedDate());
} else {
date = dateFormatter.format(Calendar.getInstance().getTime());
}
// Add the entry
if (path != null && parameter != null) {
if (ruleMap.get(path) == null) {
if (date == null) {
date = dateFormatter.format(Calendar.getInstance().getTime());
}
dateMap.put(path, date);
Map<String, Set<String>> entry = new HashMap<String,Set<String>>();
ruleMap.put(path, entry);
}
if (ruleMap.get(path).get(parameter) == null) {
ruleMap.get(path).put(parameter, new TreeSet<String>());
}
if (attackSignatures != null && attackSignatures.length > 0) {
ruleMap.get(path).get(parameter)
.addAll(Arrays.asList(attackSignatures));
}
}
if (directive == null && rule != null &&
rule.getWafRuleDirective() != null) {
directive = rule.getWafRuleDirective().getDirective();
}
// check for any of the special cases
if (rule != null && rule.getVulnerabilityDesc() != null &&
rule.getVulnerabilityDesc().startsWith("CSRF")) {
csrfStringList.add(rule.getRule());
} else if (!directoryTraversalsOn &&
rule != null && rule.getVulnerabilityDesc() != null &&
rule.getVulnerabilityDesc()
.equals(GenericVulnerability.CWE_PATH_TRAVERSAL)) {
directoryTraversalsOn = true;
} else if (!responseScrubbingOn &&
rule.getRule().equals("Response Scrubbing")) {
responseScrubbingOn = true;
} else if (!illegalMethodOn &&
rule.getRule().equals("Illegal Method")) {
illegalMethodOn = true;
}
}
Collections.sort(csrfStringList);
for (String rule : csrfStringList) {
csrfStrings.append("\n ").append(rule);
}
// this does the expansion of the big data structure
expandRules(ruleMap, dateMap, ruleTextBuilder);
StringBuilder start = new StringBuilder();
// handle special cases and expand the rest of the policy
String directoryTraversal = directoryTraversalsOn ? "enabled" : "disabled";
String responseScrubbing = responseScrubbingOn ? "true" : "false";
String illegalMethod = illegalMethodOn ? "true" : "false";
if (csrfStrings.length() == 0) {
start.append(BigIPStrings.XML_START_BEFORE_CSRF)
.append(BigIPStrings.XML_START_AFTER_CSRF
.replaceFirst("\\{directive\\}", directive)
.replaceAll("\\{responseScrubbing\\}", responseScrubbing))
.append(BigIPStrings.XML_START_CSRF_DISABLED);
} else {
start.append(BigIPStrings.XML_START_BEFORE_CSRF)
.append(csrfStrings.toString())
.append(BigIPStrings.XML_START_AFTER_CSRF
.replaceFirst("\\{directive\\}", directive)
.replaceAll("\\{responseScrubbing\\}", responseScrubbing))
.append(BigIPStrings.XML_START_CSRF_ENABLED);
}
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
start.append(BigIPStrings.XML_START_FINAL
.replaceAll("\\{date\\}",
dateFormatter.format(Calendar.getInstance().getTime()))
.replaceAll("\\{directoryTraversal\\}", directoryTraversal)
.replaceAll("\\{illegalMethod\\}", illegalMethod)
);
start.append(ruleTextBuilder);
return start.toString();
}
/**
* Take the map structure that is constructed and append all of the rule text
* onto the supplied StringBuilder
* @param ruleMap
* @param dateMap
* @param ruleTextBuilder
*/
private static void expandRules(Map<String, Map<String, Set<String>>> ruleMap,
Map<String, String> dateMap, StringBuilder ruleTextBuilder) {
if (ruleMap == null || dateMap == null || ruleTextBuilder == null) {
return;
}
for (Entry<String, Map<String, Set<String>>> entry : ruleMap.entrySet()) {
if (entry == null) {
continue;
}
String date = dateMap.get(entry.getKey());
ruleTextBuilder.append(BigIPStrings.TEMPLATE_URL
.replaceFirst("\\{path\\}", entry.getKey())
.replaceFirst("\\{date\\}", date));
for (Entry<String, Set<String>> paramEntry : entry.getValue().entrySet()) {
ruleTextBuilder.append(BigIPStrings.TEMPLATE_PARAM
.replaceFirst("\\{parameter\\}", paramEntry.getKey())
.replaceFirst("\\{date\\}", date));
for (String attackSignature : paramEntry.getValue()) {
ruleTextBuilder.append(BigIPStrings.TEMPLATE_ATTACK_SIGNATURE
.replaceFirst("\\{signatureNumber\\}", attackSignature));
}
ruleTextBuilder.append(BigIPStrings.TEMPLATE_PARAM_END);
}
ruleTextBuilder.append(BigIPStrings.TEMPLATE_URL_END);
}
}
public static String getEnd(List<WafRule> rules) {
if (rules == null || rules.size() == 0) {
return null;
}
// using a set ensures that we don't include the same signature set
// more than one time.
Set<String> signatureSet = new TreeSet<String>();
for (WafRule rule : rules) {
if (rule != null && rule.getVulnerability() != null &&
rule.getVulnerability().getGenericVulnerability() != null &&
rule.getVulnerability().getGenericVulnerability().getName() != null &&
vulnTypeSigSetMap.get(rule.getVulnerability()
.getGenericVulnerability().getName()) != null) {
signatureSet.add(vulnTypeSigSetMap.get(rule.getVulnerability()
.getGenericVulnerability().getName()));
}
}
String signatureString = "";
if (signatureSet.size() > 0) {
StringBuilder signatures = new StringBuilder();
for (String signature : signatureSet) {
signatures.append(BigIPStrings.TEMPLATE_SIGNATURE_SET.replaceFirst("\\{id\\}", signature));
}
signatureString = signatures.toString();
}
// Java complains about super long strings, so I cut it into pieces.
return BigIPStrings.XML_END_BEFORE_SIGNATURES +
signatureString +
BigIPStrings.XML_AFTER_SIGNATURES_1 +
BigIPStrings.XML_AFTER_SIGNATURES_2 +
BigIPStrings.XML_AFTER_SIGNATURES_3 +
BigIPStrings.XML_AFTER_SIGNATURES_4 +
BigIPStrings.XML_AFTER_SIGNATURES_5 +
BigIPStrings.XML_AFTER_SIGNATURES_6;
}
}