From 7add09df599faaf044218d637e36f819c8050fe6 Mon Sep 17 00:00:00 2001
From: Jan Lieskovsky <jlieskov@redhat.com>
Date: Mon, 18 Nov 2013 18:00:27 +0100
Subject: [PATCH] [Fedora] Include directory structure to support remediations.
 [Fedora] Add text file pattern remediation script generator. [Fedora] Add
 remediation for "Set password minimum length in login.defs" rule

Signed-off-by: Jan Lieskovsky <jlieskov@redhat.com>
---
 Fedora/Makefile                                    |   5 +
 .../bash/accounts_password_minlen_login_defs.sh    |   9 ++
 Fedora/input/fixes/bash/templates/Makefile         |   8 ++
 ...replace_or_append_pattern_value_in_text_file.sh | 103 +++++++++++++++++++++
 .../input/fixes/bash/templates/output/.gitignore   |   2 +
 Fedora/input/fixes/bash/templates/support.sh       |   9 ++
 Fedora/input/profiles/common.xml                   |   1 +
 Fedora/scap-security-guide.spec                    |   8 +-
 Fedora/transforms/combinefixes.py                  |  49 ++++++++++
 Fedora/transforms/xccdf-addfixes.xslt              |  66 +++++++++++++
 10 files changed, 259 insertions(+), 1 deletion(-)
 create mode 100644 Fedora/input/fixes/bash/accounts_password_minlen_login_defs.sh
 create mode 100644 Fedora/input/fixes/bash/templates/Makefile
 create mode 100755 Fedora/input/fixes/bash/templates/generators/create_replace_or_append_pattern_value_in_text_file.sh
 create mode 100644 Fedora/input/fixes/bash/templates/output/.gitignore
 create mode 100644 Fedora/input/fixes/bash/templates/support.sh
 create mode 100755 Fedora/transforms/combinefixes.py
 create mode 100644 Fedora/transforms/xccdf-addfixes.xslt

diff --git a/Fedora/Makefile b/Fedora/Makefile
index 1bdb6fe..0072a67 100644
--- a/Fedora/Makefile
+++ b/Fedora/Makefile
@@ -25,6 +25,10 @@ shorthand-guide: set-fedora-release
 shorthand2xccdf: shorthand-guide
 	xsltproc -o $(OUT)/unlinked-unresolved-$(PROD)-xccdf.xml $(TRANS)/shorthand2xccdf.xslt $(OUT)/$(ID)-$(PROD)-shorthand.xml
 	oscap xccdf resolve -o $(OUT)/unlinked-$(PROD)-xccdf.xml $(OUT)/unlinked-unresolved-$(PROD)-xccdf.xml
+	# Include fixes
+	./$(TRANS)/combinefixes.py $(IN)/fixes/bash/ $(OUT)/bash-remediations.xml
+	xsltproc -stringparam fixes "../$(OUT)/bash-remediations.xml" -o $(OUT)/unlinked-$(PROD)-xccdf.xml $(TRANS)/xccdf-addfixes.xslt $(OUT)/unlinked-$(PROD)-xccdf.xml
+	xmllint --format --output $(OUT)/unlinked-$(PROD)-xccdf.xml $(OUT)/unlinked-$(PROD)-xccdf.xml
 
 checks:
 	xmlwf $(IN)/checks/*.xml
@@ -65,4 +69,5 @@ eval-common: content
 clean:
 	rm -f $(OUT)/*.xml $(OUT)/*.html $(OUT)/*.xhtml $(OUT)/*.pdf  $(OUT)/*.spec $(OUT)/*.tar $(OUT)/*.gz $(OUT)/*.ini $(OUT)/*.csv
 	rm -f $(OUT)/$(FEDORA_GUIDE_XSLT)
+	rm -f $(IN)/fixes/bash/templates/output/*.sh
 	rm -rf $(DIST)/content
diff --git a/Fedora/input/fixes/bash/accounts_password_minlen_login_defs.sh b/Fedora/input/fixes/bash/accounts_password_minlen_login_defs.sh
new file mode 100644
index 0000000..c950c3a
--- /dev/null
+++ b/Fedora/input/fixes/bash/accounts_password_minlen_login_defs.sh
@@ -0,0 +1,9 @@
+source ./templates/support.sh
+populate var_accounts_password_minlen_login_defs
+
+grep -q ^PASS_MIN_LEN /etc/login.defs && \
+sed -i "s/PASS_MIN_LEN.*/PASS_MIN_LEN\t$var_accounts_password_minlen_login_defs/g" /etc/login.defs
+if ! [ $? -eq 0 ]
+then
+  echo -e "PASS_MIN_LEN\t$var_accounts_password_minlen_login_defs" >> /etc/login.defs
+fi
diff --git a/Fedora/input/fixes/bash/templates/Makefile b/Fedora/input/fixes/bash/templates/Makefile
new file mode 100644
index 0000000..d63e5c4
--- /dev/null
+++ b/Fedora/input/fixes/bash/templates/Makefile
@@ -0,0 +1,8 @@
+compare:
+	diff output/ ../ | grep -v "Only in ../"
+
+copy:
+	cp output/*.sh ../
+
+clean:
+	rm -f output/*.sh
diff --git a/Fedora/input/fixes/bash/templates/generators/create_replace_or_append_pattern_value_in_text_file.sh b/Fedora/input/fixes/bash/templates/generators/create_replace_or_append_pattern_value_in_text_file.sh
new file mode 100755
index 0000000..d4f6603
--- /dev/null
+++ b/Fedora/input/fixes/bash/templates/generators/create_replace_or_append_pattern_value_in_text_file.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+
+# SCAP "replace or append pattern value in text file based on variable"
+# remediation script generator
+#
+# Required arguments:
+# -p pattern to search content of text file for
+# -f text file to be searched
+# -v use variable as new value for the pattern
+# -o name of resulting remediation script output file
+#
+# Optional arguments:
+# -s request pattern match from the start of the line
+#
+# Example use:
+#
+# ./create_replace_or_append_pattern_value_in_text_file.sh \
+# -p PASS_MIN_LEN \ 
+# -f /etc/login.defs \
+# -v var_accounts_password_minlen_login_defs
+# -o ../output/accounts_password_minlen_login_defs.sh
+
+# meaning:
+# a remediation script to replace 'PASS_MIN_LEN' string
+# in '/etc/login.defs' file with value of
+# '$var_accounts_password_minlen_login_defs' variable
+# would be created based on template and stored into
+# 'accounts_password_minlen_login_defs.sh' output file
+# Since no -s argument was provided the text file would
+# be searched for any occurrence of 'PASS_MIN_LEN' row
+# -----------------------------------------------------
+
+# Define script routines below
+# ----------------------------
+
+#1 Usage function definition
+
+function usage {
+  echo -e "Usage:\n\t$0 [-s] -p pattern -f file -v variable -o output_script\n"
+  echo -e "-p pattern to search file for\t\t\t-f text file to be searched"
+  echo -e "-v name of the variable to be considered\t[-s] match the start of the line or not"
+  echo -e "   to hold new value for pattern\t\t-o name of the resulting remediation script"
+}
+
+#2 Remediation script generator function definition
+# Expects pattern as first argument, file as second argument,
+# and variable name as third argument
+
+function generate_replace_or_append_remediation_script {
+
+  # Save the provided args to local variables
+  local pattern=$1 file=$2 variable=$3 remediation=$4
+
+  pattern_prefix=''
+
+  if [ $MATCH_START_OF_LINE ]
+  then
+    pattern_prefix='^'
+  fi
+
+  # Generate the remediation script
+  cat > $REMEDIATION << EOF
+source ./templates/support.sh
+populate $variable
+
+grep -q $pattern_prefix$pattern $file && \\
+sed -i "s/$pattern.*/$pattern\t\$$variable/g" $file
+if ! [ \$? -eq 0 ]
+then
+  echo -e "$pattern\t\$$variable" >> $file
+fi
+EOF
+
+}
+
+
+##### Main body of the generator script #####
+
+# Initialize variables
+PATTERN=false FILE= false VARIABLE=false 
+MATCH_START_OF_LINE=false REMEDIATION=false
+
+# Specify valid options and action for each of them
+while getopts "p:f:v:so:" opt
+do
+  case $opt in
+    p ) PATTERN=$OPTARG  ;;
+    f ) FILE=$OPTARG     ;;
+    v ) VARIABLE=$OPTARG ;;
+    s ) MATCH_START_OF_LINE=true ;;
+    o ) REMEDIATION=$OPTARG ;;
+    * ) usage; exit
+  esac
+done
+
+# Provided input wasn't correct. Just display usage message
+if [ -z $PATTERN ] || [ -z $FILE ] || [ -z $VARIABLE ] || [ -z $REMEDIATION ]
+then
+  usage
+# Command line was correct, generate the remediation script
+else
+  generate_replace_or_append_remediation_script $PATTERN $FILE $VARIABLE $REMEDIATION
+fi
diff --git a/Fedora/input/fixes/bash/templates/output/.gitignore b/Fedora/input/fixes/bash/templates/output/.gitignore
new file mode 100644
index 0000000..041cc36
--- /dev/null
+++ b/Fedora/input/fixes/bash/templates/output/.gitignore
@@ -0,0 +1,2 @@
+# files to ignore
+*.sh
diff --git a/Fedora/input/fixes/bash/templates/support.sh b/Fedora/input/fixes/bash/templates/support.sh
new file mode 100644
index 0000000..e25ce4d
--- /dev/null
+++ b/Fedora/input/fixes/bash/templates/support.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+function populate {
+# code to populate environment variables needed (for unit testing)
+if [ -z "${!1}" ]; then
+	echo "$1 is not defined. Exiting."
+	exit
+fi
+}
diff --git a/Fedora/input/profiles/common.xml b/Fedora/input/profiles/common.xml
index 8997e79..e7d7d25 100644
--- a/Fedora/input/profiles/common.xml
+++ b/Fedora/input/profiles/common.xml
@@ -24,6 +24,7 @@
     <select idref="no_netrc_files" selected="true"/>
     <!-- Set Password Expiration Parameters section rules -->
     <select idref="accounts_password_minlen_login_defs" selected="true"/>
+    <refine-value idref="var_accounts_password_minlen_login_defs" selector="12" />
     <select idref="accounts_minimum_age_login_defs" selected="true"/>
     <select idref="accounts_maximum_age_login_defs" selected="true"/>
     <select idref="accounts_password_warn_age_login_defs" selected="true"/>
diff --git a/Fedora/scap-security-guide.spec b/Fedora/scap-security-guide.spec
index 85f9e2d..c418b93 100644
--- a/Fedora/scap-security-guide.spec
+++ b/Fedora/scap-security-guide.spec
@@ -5,7 +5,7 @@
 # file one level up - in the main scap-security-guide directory (instead of
 # this one).
 
-%global	fedorassgversion	3
+%global	fedorassgversion	4.rc1
 
 Name:		scap-security-guide
 Version:	0.1.%{fedorassgversion}
@@ -53,6 +53,12 @@ cp -a Fedora/input/auxiliary/scap-security-guide.8 %{buildroot}%{_mandir}/en/man
 %doc Fedora/LICENSE Fedora/output/ssg-fedora-guide.html
 
 %changelog
+* Mon Nov 18 2013 Jan iankko Lieskovsky <jlieskov@redhat.com> 0.1.4.rc1-1
+- Include directory structure to support remediations
+- Add SCAP "replace or append pattern value in text file based on variable"
+  remediation script generator
+- Add remediation for "Set Password Minimum Length in login.defs" rule
+
 * Mon Nov 18 2013 Jan iankko Lieskovsky <jlieskov@redhat.com> 0.1.3-1
 - Update versioning scheme - move fedorassgrelease to be part of
   upstream version. Rename it to fedorassgversion to avoid name collision
diff --git a/Fedora/transforms/combinefixes.py b/Fedora/transforms/combinefixes.py
new file mode 100755
index 0000000..71fbe6f
--- /dev/null
+++ b/Fedora/transforms/combinefixes.py
@@ -0,0 +1,49 @@
+#!/usr/bin/python
+
+import sys, os, re, lxml.etree as etree
+
+def substitute_vars(fix):
+	# brittle and troubling code to assign environment vars to XCCDF values
+	m = re.match("(\s*source\s+\S+)\n+(\s*populate\s+)(\S+)\n(.*)", fix.text, re.DOTALL)
+	if not m: 
+		# no need to alter fix.text
+		return
+	# otherwise, create node to populate environment variable
+	varname = m.group(3)
+	mainscript = m.group(4)
+	fix.text = varname + "=" + '"'
+	# new <sub> element to reference XCCDF variable
+	xccdf_sub = etree.SubElement(fix, "sub", idref=varname)
+	xccdf_sub.tail = '"' + mainscript
+	fix.append(xccdf_sub)
+	
+
+def main():
+	if len(sys.argv) < 2:
+		print "Provide a directory name, which contains the fixes."
+		sys.exit(1)
+		
+	fixdir = sys.argv[1]
+	output = sys.argv[2]
+
+	fixcontent = etree.Element("fix-content", system="urn:xccdf:fix:script:sh", xmlns="http://checklists.nist.gov/xccdf/1.1")
+	fixgroup = etree.SubElement(fixcontent, "fix-group", id="bash", system="urn:xccdf:fix:script:sh", xmlns="http://checklists.nist.gov/xccdf/1.1")
+	
+	for filename in os.listdir(fixdir):
+		if filename.endswith(".sh"):
+			# create and populate new fix element based on shell file
+			fixname = os.path.splitext(filename)[0]
+			fix = etree.SubElement(fixgroup, "fix", rule=fixname)
+			with open( fixdir + "/" + filename, 'r') as f:
+				# assignment automatically escapes shell characters for XML
+				fix.text = f.read()
+				# replace instance of bash function "populate" with XCCDF variable substitution
+				substitute_vars(fix)
+
+	tree = etree.ElementTree(fixcontent)
+	tree.write(output, pretty_print=True)
+
+	sys.exit(0)
+		
+if __name__ == "__main__":
+	main()
diff --git a/Fedora/transforms/xccdf-addfixes.xslt b/Fedora/transforms/xccdf-addfixes.xslt
new file mode 100644
index 0000000..0a7e1e2
--- /dev/null
+++ b/Fedora/transforms/xccdf-addfixes.xslt
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xccdf="http://checklists.nist.gov/xccdf/1.1" xmlns:xhtml="http://www.w3.org/1999/xhtml" exclude-result-prefixes="xccdf">
+
+<!-- This transform expects a stringparam "fixes" specifying a filename
+     containing a list of fixes.  It inserts these into the Rules 
+     specified inside the fixes file. -->
+
+<xsl:variable name="fixgroup" select="document($fixes)/xccdf:fix-content/xccdf:fix-group" />
+<xsl:variable name="fixsystem" select="$fixgroup/@system"/>
+<xsl:variable name="fixcommongroup" select="document($fixes)/xccdf:fix-content/xccdf:fix-common-group" />
+
+  <xsl:template match="xccdf:Rule">
+    <xsl:copy>
+	  <!-- deal with the fact that oscap demands fixes stand only before checks -->
+      <xsl:apply-templates select="@*|node()[not(self::xccdf:check)]"/>
+
+      <xsl:variable name="rule_id" select="@id"/>
+      <xsl:for-each select="$fixgroup/xccdf:fix"> 
+        <xsl:if test="@rule=$rule_id">
+          <xsl:element name="fix" namespace="http://checklists.nist.gov/xccdf/1.1">
+          <xsl:attribute name="system"><xsl:value-of select="$fixsystem"/></xsl:attribute>
+          <xsl:apply-templates select="node()"/>
+          </xsl:element>
+        </xsl:if>
+      </xsl:for-each> 
+      <xsl:apply-templates select="node()[self::xccdf:check]"/>
+
+    </xsl:copy>
+  </xsl:template>
+
+  <xsl:template match="xccdf:Benchmark">
+    <xsl:copy>
+
+      <!-- plain-text elements must appear in sequence -->
+      <xsl:apply-templates select="@*"/>
+      <xsl:apply-templates select="xccdf:status"/>
+      <xsl:apply-templates select="xccdf:dc-status"/>
+      <xsl:apply-templates select="xccdf:title"/>
+      <xsl:apply-templates select="xccdf:description"/>
+      <xsl:apply-templates select="xccdf:notice"/>
+      <xsl:apply-templates select="xccdf:front-matter"/>
+      <xsl:apply-templates select="xccdf:rear-matter"/>
+      <xsl:apply-templates select="xccdf:reference"/>
+
+      <xsl:for-each select="$fixcommongroup/xccdf:fix-common">
+        <xsl:variable name="fix_common_id" select="@id"/>
+        <xsl:element name="plain-text" namespace="http://checklists.nist.gov/xccdf/1.1">
+        <xsl:attribute name="id"><xsl:value-of select="$fix_common_id"/></xsl:attribute>
+        <xsl:value-of select="text()"/>
+        </xsl:element>
+      </xsl:for-each> 
+      <xsl:apply-templates select="node()[not(self::xccdf:status|self::xccdf:dc-title|self::xccdf:title|self::xccdf:description|self::xccdf:notice|self::xccdf:front-matter|self::xccdf:rear-matter|self::xccdf:reference)]"/>
+<!--
+      <xsl:apply-templates select="node()[not(self::xccdf:status)]"/>
+-->
+    </xsl:copy>
+  </xsl:template>
+
+  <!-- copy everything else through to final output -->
+  <xsl:template match="@*|node()">
+    <xsl:copy>
+      <xsl:apply-templates select="@*|node()" />
+    </xsl:copy>
+  </xsl:template>
+
+</xsl:stylesheet>
-- 
1.8.3.1

