I spend a considerable amount of time in the QualysGuard web interface editing remediation tickets. It’s one of those admin tasks that you dread doing, but have to do.

If you’ve ever used the QualysGuard web user interface, you’ll know that it’s not particularly fast. Aside from the speed of the UI, there are also functional limitations. For example, you can only display 500 tickets per page. So, to edit 5,000 tickets, you need to browse through 10 pages and select all tickets on each page. Also, if you want to select tickets by ticket number or QID, you can only select one ticket number or QID at a time.

Thankfully, Qualys have provided a powerful API that overcomes these limitations. I have written a PowerShell script that takes full advantage of the ticket_edit.php API function to edit up to 20,000 remediation tickets at a time.

A detailed help section is provided in the script. In a PowerShell console, type: get-help .\edit-qualysticket.ps1 –detailed

You will need to configure the $urlQualysLocation variable to set the QualysGuard URL for your location. For more information on the Qualys API, visit here: https://community.qualys.com/community/developer

<#
.SYNOPSIS
This Powershell script uses the QualysGuard API to edit remedation tickets.

.DESCRIPTION
The script uses the ticket_edit.php API function in QualysGuard to edit remediation tickets in a QualysGuard subscription. This function allows you to change the ticket assignee, open and close tickets, and add comments to tickets. Several input parameters are available for ticket selection. At least one ticket selection parameter is required, and one edit parameter is required.

The API is more powerful than the QualysGuard User Interface in how you can select tickets for editing, e.g. ability to select multiple ticket numbers or QIDs at a time, ability to select up to 20,000 tickets at a time without having to browse and select 500 tickets per page using the web UI. 

.PARAMETER TicketNumbers
Tickets with certain ticket numbers. Specify one or more ticket numbers and/or ranges. Use a comma (,) to separate multiple tickets

.PARAMETER SinceTicketNumber
Tickets since a certain ticket number. Specify the lowest ticket number to be selected. Selected tickets will have numbers greater than or equal to the ticket number specified.

.PARAMETER UntilTicketNumber
Tickets until a certain ticket number. Specify the highest ticket number to be selected. Selected tickets will have numbers less than or equal to the ticket number specified.

.PARAMETER Overdue
Tickets that are overdue or not overdue. When not specified, overdue and non-overdue tickets are selected. Specify 1 to select only overdue tickets. Specify 0 to select only tickets that are not overdue.

.PARAMETER Invalid
Tickets that are invalid or valid. When not specified, both valid and invalid tickets are selected. Specify 1 to select only invalid tickets. Specify 0 to select only valid tickets. You can select invalid tickets owned by other users, not yourself.

.PARAMETER ModifiedSinceDateTime
Tickets modified since a certain date/time. Specify a date (required) and time (optional) since tickets were modified. Tickets modified on or after the date/time are selected. The start date/time is specified in YYYY-MMDD[THH:MM:SSZ] format (UTC/GMT), like “2006-01-01” or “2006-05-25T23:12:00Z”.

.PARAMETER UnModifiedSinceDateTime
Tickets not modified since a certain date/time. Specify a date (required) and time (optional) since tickets were not modified. Tickets not modified on or after the date/time are selected. The date/time is specified in YYYY-MM-DD[THH:MM:SSZ] format (UTC/GMT), like “2006-01-01” or “2006-05-25T23:12:00Z”.

.PARAMETER IPs
Tickets on hosts with certain IP addresses. Specify one or more IP addresses and/or ranges. Multiple entries are comma separated.

.PARAMETER DNSContains
Tickets on hosts that have a NetBIOS host name which contains a certain text string. Specify a text string to be used. This string may include a maximum of 100 characters.

.PARAMETER NETBIOSContains
Tickets on hosts that have a NetBIOS host name which contains a certain text string. Specify a text string to be used. This string may include a maximum of 100 characters.

.PARAMETER VendorRefContains
Tickets for vulnerabilities that have a vendor reference which contains a certain text string. Specify a text string. This string may include a maximum of 100 characters.

.PARAMETER TicketAssignee
Tickets with a certain assignee. Specify the user login of an active user account.

.PARAMETER States
Tickets with certain ticket state/status. Specify one or more state/status codes from (OPEN,RESOLVED,CLOSED,IGNORED). Use a comma (,) to separate multiple. A valid value is OPEN (for state/status Open or Open/Reopened), RESOLVED (for state Resolved), CLOSED(for state/status Closed/Fixed), or IGNORED (for state/status Closed/Ignored). 

.PARAMETER AssetGroups
Tickets on hosts with IP addresses which are defined in certain asset groups. Specify the title of one or more asset groups. Multiple asset groups are comma separated. The title “All” may be specified to select all IP addresses in the user account.

.PARAMETER VulnSevereties
Tickets for vulnerabilities with certain severity levels. Specify one or more severity levels. Multiple levels are comma separated. (1,2,3,4,5)

.PARAMETER PotVulnSevereties
Tickets for potential vulnerabilities with certain severity levels. Specify one or more severity levels. Multiple levels are comma separated. (1,2,3,4,5)

.PARAMETER QIDs
Tickets for vulnerabilities with certain QIDs (Qualys IDs). Specify one or more QIDs. A maximum of 10 QIDs may be specified. Multiple QIDs are comma separated.

.PARAMETER VulnTitleContains
Tickets for vulnerabilities that have a title which contains a certain text string. The vulnerability title is defined in the KnowledgeBase. Specify a text string and enclose in double quotes ("insert text here"). This string may include a maximum of 100 characters.

.PARAMETER VulnDetailsContains
Tickets for vulnerabilities that have vulnerability details which contain a certain text string. Vulnerability details provide descriptions for threat, impact, solution and results (scan test results, when available). Specify a text string and enclose in double quotes ("insert text here"). This string may include a maximum of 100 characters.

.PARAMETER ChangeAssignee
Used to change the ticket assignee, specified by user login, in all selected tickets. The assignee’s account must have a user role other than Contact, and the hosts associated with the selected tickets must be in the user account.

.PARAMETER ChangeState
Used to change the ticket state/status to the specified state/status in all selected tickets.(OPEN,RESOLVED,IGNORED) A valid value is OPEN (for state/status Open and Open/Reopened),RESOLVED (for state Resolved), or IGNORED (for state/status Closed/Ignored). 

.PARAMETER AddComment
Used to add a comment in all selected tickets. The comment text may include a maximum of 2,000 characters. Enclose all comments in double quotes ("insert text here").

.EXAMPLE
Edit-QualysTickets.ps1 -QIDs 76543 -ChangeState RESOLVED -AddComment "This vulnerability has been addressed. Changing ticket state to RESOLVED." 

.EXAMPLE
Edit-QualysTickets.ps1 -TicketNumbers 12345,23456 -Assignee usrts-ab -States OPEN,RESOLVED -ChangeAssignee usrts-bc -ChangeState IGNORED -AddComment "Re-assigning tickets from user usrts-ab to usrts-bc and changing state to IGNORED." 

.EXAMPLE
Edit-QualysTickets.ps1 -IPs 192.168.1,192.168.2 -ModifiedSinceDateTime 2011-09-16T00:00:00Z -States RESOLVED -VulnSevereties 4,5 -VulnTitleContains "Adobe Reader" -ChangeState OPEN

.NOTES
Requires a QualysGuard account with permission to edit tickets for a given asset group. Managers and Unit Managers have permission to run this API function.

A maximum of 20,000 tickets can be edited in one request. The QualysGuard credentials are transmitted using the "Basic Authentication Scheme" over HTTPS.

The Script saves the Qualys response in XML format and writes output to a log file. These files are saved in the %temp% directory. The $urlQualysLocation variable must be configured with the QualysGuard URL for your subscription location.

.LINK
https://powersheller.wordpress.com

#>

Param ([ValidateCount(1,1000)]
[string[]]$TicketNumbers,
[string]$SinceTicketNumber,
[string]$UntilTicketNumber,
[string]$TicketAssignee,
[ValidateSet("0","1")]
[string]$Overdue,
[ValidateSet("0","1")]
[string]$Invalid,
[ValidateSet("OPEN","RESOLVED","CLOSED","IGNORED")] 
[string[]]$States,
[string]$ModifiedSinceDateTime,
[string]$UnModifiedSinceDateTime,
[string[]]$IPs,
[string[]]$AssetGroups,
[ValidateLength(1,100)]
[string]$DNSContains,
[ValidateLength(1,100)]
[string]$NetbiosContains,
[ValidateSet("1","2","3","4","5")]
[string[]]$VulnSevereties,
[ValidateSet("1","2","3","4","5")]
[string[]]$PotVulnSevereties,
[ValidateCount(1,10)]
[string[]]$QIDs,
[ValidateLength(1,100)]
[string]$VulnTitleContains,
[ValidateLength(1,100)]
[string]$VulnDetailsContains,
[ValidateLength(1,100)]
[string]$VendorRefContains,
[string]$ChangeAssignee,
[ValidateSet("OPEN","RESOLVED","IGNORED")] 
[string]$ChangeState,
[ValidateLength(1,2000)]
[string]$AddComment
)

#*=============================================
#* VARIABLE DECLARATION
#*=============================================

# Variables: Qualys URLs
$urlQualysLocation = "qualysapi.qualys.eu"
#$urlQualysLocation = "qualysapi.qualys.com"
$urlQualys = "https://$urlQualysLocation/msp"
$urlQualysTicketEdit = "$urlQualys/ticket_edit.php?"
$xmlTicketEdit = Join-path $env:temp "Edit-QualysTicket.xml"

# Set the parameters to be passed to the Ticket_edit.php function if they are specified as script parameters.
### Ticket Selection parameters ###
If ($ticketNumbers -ne $null) { $paramTicketNumbers = "&ticket_numbers="+($ticketNumbers -join ",") }
If ($sinceTicketNumber -ne "") { $paramSinceTicketNumber = "&since_ticket_number="+$sinceTicketNumber }
If ($untilticketNumber -ne "") { $paramUntilticketNumber = "&since_ticket_number="+$untilticketNumber }
If ($overdue -ne "") { $paramOverdue = "&overdue="+$overdue }
If ($invalid -ne "") { $paramInvalid = "&overdue="+$invalid }
If ($ticketAssignee -ne "") { $paramAssignee = "&ticket_assignee="+$ticketAssignee }
If ($states -ne $null) { $paramStates = "&states="+($states -join ",") }
If ($modifiedSinceDateTime -ne "") { $parammodifiedSinceDateTime = "&modified_since_datetime="+$modifiedSinceDateTime }
If ($unmodifiedSinceDateTime -ne "") { $paramUnmodifiedSinceDateTime = "&unmodified_since_datetime="+$unmodifiedSinceDateTime }
If ($ips -ne $null) { $paramIps = "&ips="+($ips -join ",") }
If ($assetGroups -ne $null) { $paramAssetGroups = "&asset_groups="+($assetGroups -join ",") }
If ($dnsContains -ne "") { $paramDnsContains = "&dns_contains="+$dnsContains }
If ($netbiosContains -ne "") { $paramNetbiosContains = "&netbios_contains="+$netbiosContains }
If ($vulnSevereties -ne $null) { $paramVulnSevereties = "&vuln_severities="+($vulnSevereties -join ",") }
If ($potVulnSevereties -ne $null) { $paramPotVulnSevereties = "&potential_vuln_severities="+($potVulnSevereties -join ",") }
If ($qids -ne $null) { $paramQids = "&qids="+($qids -join ",") }
If ($vulnTitleContains -ne "") { $paramVulnTitleContains = "&vuln_title_contains="+$vulnTitleContains }
If ($vulnDetailsContains -ne "") { $paramVulnDetailsContains = "&vuln_details_contains="+$vulnDetailsContains }
If ($vendorRefContains -ne "") { $paramVendorRefContains = "&vendor_ref_contains="+$vendorRefContains }
### Ticket Action parameters ###
If ($changeAssignee -ne "") { $paramChangeAssignee = "&change_assignee="+$changeAssignee }
If ($changeState -ne "") { $paramChangeState = "&change_state="+$changeState }
If ($addComment -ne "") { $paramAddComment = "&add_comment="+$addComment }

$urlQualysTicketEditFinal = $urlQualysTicketEdit+$paramTicketNumbers+$paramSinceTicketNumber+$paramUntilticketNumber+$paramOverdue+$paramInvalid+$parammodifiedSinceDateTime+$paramUnmodifiedSinceDateTime+$paramIps+$paramTicketAssignee+$paramStates+$paramAssetGroups+$paramDnsContains+$paramNetbiosContains+$paramVulnSevereties+$paramPotVulnSevereties+$paramQids+$paramVulnTitleContains+$paramVulnDetailsContains+$paramVendorRefContains+$paramChangeAssignee+$paramChangeState+$paramAddComment

#*=============================================
#* END VARIABLE DECLARATION
#*=============================================

#*=============================================
#* FUNCTION LISTINGS
#*=============================================

Function Exit-Script {
Write-Log -Message "Exiting Script."
Exit
}

# Function to write output to the console and to log file
Function Write-Log {
	PARAM(
	[String]$Message,
	[String]$Path = "$env:temp\$(Split-Path $MyInvocation.ScriptName -Leaf).log"
	)
	
	Write-Host $message 
	$message | Out-File -FilePath $Path -Append -NoClobber
	} #end function

# Function to authenticate to Qualys, POST a URL request and retreive the response.
Function Connect-QualysAPI {
	# Parameters required: Qualys URL string and output XML file
	param($url,$xml)
	Write-Log -Message "URL request: $url"
	Write-Log -Message "Sending Request to Qualys API.."
	# Call Web Service and ignore erros with invalid, untrusted or expired SSL CertIficates (we know we are dealing with Qualys) - this only affects the current PowerShell runspace
	[System.Net.ServicePointManager]::ServerCertIficateValidationCallback = {$true}
	# Set up the webclient
	$webClient = new-object System.Net.WebClient
	$webClient.Credentials = new-object System.Net.NetworkCredential($username,$password)

	# Trap exceptions when calling DownloadString and call the Exit-Script function
	trap [System.Net.WebException] { 
		$exceptionMessage = $_.Exception.Message
		$exceptionName = $_.Exception.GetType().FullName
		Write-Log -Message "$exceptionName: $exceptionMessage"
		Exit-Script 
		}
	
	# Send request URL to Qualys, then wait until the request has been processed and downloaded fully.
	$xml = $webClient.DownloadString($url) | Out-File -FilePath $xml
	
} # End Function Connect-QualysAPI

# Function to parse the XML response response from Edit-QualysTicket
Function Get-QualysResponse {
	Param ($xmlResponse)
	Write-Log -Message "Checking response from Qualys..."
	# Read the Qualys XML response
	[xml]$xml = (Get-Content $xmlResponse)
		# If the request to edit the ticket resulted in a change, parse the results.
		If ($xml.Ticket_Edit_Output) {
			If ($xml.Ticket_Edit_Output[1].Changes)	{
				Write-Log -Message "Parsing response from Qualys..."
				$ticketNetbios = $xml.Ticket_Edit_Output[1].Header.Where.Netbios_Contains.InnerText
				$ticketChangesCount = $xml.Ticket_Edit_Output[1].Changes.getAttribute("count")
				$ticketChanges = $xml.Ticket_Edit_Output[1].Changes.TICKET_NUMBER_LIST
				$ticketSkippedCount = $xml.Ticket_Edit_Output[1].Skipped.getAttribute("count")
				$ticketSkipped = $xml.Ticket_Edit_Output[1].Skipped.TICKET_LIST.TICKET
					
				# If there was a change to one or more tickets, get the details
				If ($ticketChangesCount -gt 0) {
					$ticketChangesArray = @()
					Foreach ($ticketNumber in $ticketChanges.TICKET_NUMBER.InnerText) {			
						$ticketChangesNumber = $ticketChanges.TICKET_NUMBER
						$ticketChangesArray += $ticketChangesNumber
					}
					
					# Format the ticket numbers changed in to comma separated string (for printing on one line of log file).	
					$ticketChangesString = [string]::join(",", $ticketChangesArray)
					Write-Log -Message "$ticketChangesCount tickets edited."
					Write-Log -Message "Ticket Numbers changed: $ticketChangesString"										
				}
					
				# If editing one or more tickets was skipped, get the details and reason why.
				ElseIf ($ticketSkippedCount -gt 0) {
					$ticketSkippedArray = @()
					Write-Log -Message "$ticketNetbios - `nNumber of Tickets Skipped:$ticketCountSkipped `nTicket Numbers skipped:"
						Foreach ($ticketNumber in $ticketSkipped) {
							$ticketSkippedNumber = $ticketSkipped.Number.InnerText
							$ticketSkippedReason = $ticketSkipped.Reason.InnerText
							$ticketSkippedArray += "$ticketSkippedNumber:$ticketSkippedReason"
						}
					# Format the ticket numbers skipped along with the reason for skipping as a string.		
					$ticketSkippedString = [string]::join(",", $ticketSkippedArray)
					Write-Log -Message "The following tickets were skipped: $ticketSkippedString"
				}		
					
				# If no tickets were changed or skipped, something was wrong with the request, e.g. ticket state not Uppercase
				Else {
					Write-Log -Message "$ticketNetbios - No tickets were altered. There may not be any open tickets to edit."
				}
			}
			# If Qualys Returns an error, parse the error output
			ElseIf ($xml.Ticket_Edit_Output[1].Error) {
				Write-Log -Message "Parsing the Qualys XML error response..."
				$errorNumber = $xml.Ticket_Edit_Output[1].Error.getAttribute("number")
				$errorResponse = $xml.Ticket_Edit_Output[1].Error.InnerXml
				Write-Log -Message "Error: $errorNumber:$errorResponse"
				Exit-Script
			}	
		}
		
		# If Qualys returned a Generic Return form, call the Get-QualysGenericReturn function
		ElseIf ($xml.GENERIC_RETURN) {	
			Write-Log -Message "Parsing the Qualys XML generic response..."
			$genericApiName = $xml.GENERIC_RETURN[1].API.getAttribute("name")
			$genericapiUserName = $xml.GENERIC_RETURN[1].API.getAttribute("username")
			$genericApiTime = $xml.GENERIC_RETURN[1].API.getAttribute("at")
			$genericReturnStatus = $xml.GENERIC_RETURN[1].Return.getAttribute("status")
			$genericReturnNumber = $xml.GENERIC_RETURN[1].Return.getAttribute("number")
			$genericReturnDetails = $xml.GENERIC_RETURN[1].Return.InnerXml
			Write-Log -Message "API failure: $genericApiName `nUser: $genericapiUserName `nTime: $genericApiTime"
			Write-Log -Message "Status: $genericReturnStatus `nError Number: $genericReturnNumber `nDetails: $genericReturnDetails"

			# Call the script exit function If the return status is "failed"
			If ($genericReturnStatus -imatch "FAILED") {	
				Exit-Script
			} 
		}
		
		Else {
			Write-Log -Message "Error: Qualys request was unsuccessful. Check parameters."
			Exit-Script
		}	

} # End Function Get-QualysResponse

Function Invoke-QualysTicketEdit {

Try {
	$credential = Get-Credential
}
Catch {
	Write-Log -Message "Error: Credentials are required to authenticate to Qualys"
	Exit-Script
}
$networkCredential = $credential.GetNetworkCredential()
$username = $networkCredential.UserName
$password = $networkCredential.Password

Connect-QualysAPI $urlQualysTicketEditFinal $xmlTicketEdit

Get-QualysResponse $xmlTicketEdit

Exit-Script
}

#*=============================================
#* END FUNCTION LISTINGS
#*=============================================


#*=============================================
#* SCRIPT BODY
#*=============================================

Invoke-QualysTicketEdit

#*=============================================
#* END SCRIPT BODY
#*=============================================
Advertisements

I’m using Configuration Manager 2012 Beta 2 with MDT 2012 Beta 1 to create task sequences for OSD. This error bugged me for days before I found a fix. The error code 0x80004005 is apparently an access denied error. Most of the forums on this error point to certificate issues or permissions issues. Having checked the certificates and permissions, re-created the task sequence, re-created packages and re-distributed everything I was still getting the same error.

image

The SMSTS.LOG file looks like this:

<![LOG[Executing Policy Body Request.]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2010">
<![LOG[Using Authenticator for policy]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="1" thread="1064" file="libsmsmessaging.cpp:4448">
<![LOG[CLibSMSMessageWinHttpTransport::Send: URL: SCCMServer:80  GET /SMS_MP/.sms_pol?ScopeId_647BE467-0336-4ECD-9411-6BEDD13345A/AuthList_4b3c67e6-43bb-4f8d-8362-3504308e3452/VI.SHA256:GHFSD3456DHDSGH91FE949D8A0445EFA08701D81C984564]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="1" thread="1064" file="libsmsmessaging.cpp:8720">
<![LOG[Request was succesful – 200]LOG]!><time="09:13:02.877-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="libsmsmessaging.cpp:9046">
<![LOG[cannot load compressed XML policy]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="3" thread="1064" file="libsmsmessaging.cpp:4617">
<![LOG[oPolicy.RequestPolicy((GetPolicyFlags() & POLICY_SECURE) != 0, (GetPolicyFlags() & POLICY_COMPRESS) != 0), HRESULT=80004005 (e:\nts_sccm_retail\sms\framework\tscore\tspolicy.cpp,2014)]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2014">
<![LOG[Failed to download policy ScopeId_647BE467-0336-4ECD-9411-6BEDD13345A/AuthList_4b3c67e6-43bb-4f8d-8362-3504308e3452/VI (Code 0x80004005).]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="3" thread="1064" file="tspolicy.cpp:2014">
<![LOG[DownloadPolicyBody(), HRESULT=80004005 (e:\nts_sccm_retail\sms\framework\tscore\tspolicy.cpp,2100)]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2100">

I used the free text search feature in SCCM to search all objects for the string after AuthList_ 4b3c67e6-43bb-4f8d-8362-3504308e3452

The search returned 2 related objects:

A configuration Item in Assets and Compliance.

A software update group in the Software Library.

Checking the properties of these, it appeared that there was no security scope set on the configuration item. When I tried to set the security scope, Config Mgr crashed. I looked in Assets and Compliance and the item wasn’t there. So I ran the search again and deleted the item from the search results. The item was applied to a collection that included the machine running the TS. When I re-ran the task sequence, the error was gone! It goes to show how something seemingly unrelated to the task sequence can cause problems with it.

Some more SCCM 2012 gotcha’s here:

http://scug.be/blogs/nico/archive/2011/04/26/sccm-2012-odd-issues.aspx

Introduction

Last week I came across a client-side issue applying Group Policies. It turned out to be a corrupt local group policy database file. Here’s how I went about troubleshooting and fixing the issue and then automating detection and resolution of the problem using PowerShell.

Troubleshooting

The Resultant Set of Policy (RSoP) indicated a problem processing the policy settings and advised to look in the Application event log for more details, and quite helpfully showed the exact time of the expected event to look for.

Looking at the Application event log, there was a warning event at the expected time with event ID 1202, with this description:

"Security policies were propagated with warning. 0x4b8 : An extended error has occurred."

The event description suggests searching http://support.microsoft.com which turned up this KB article:

http://support.microsoft.com/kb/324383 – Troubleshooting SCECLI 1202 Events

Scrolling down to Win32 error code for Event ID 1202 "0x4b8: An extended error has occurred", the article tell us how to enable detailed debugging to help troubleshoot the problem. It also points to this KB article:

http://support.microsoft.com/kb/278316/ – ESENT Event IDs 1000, 1202, 412, and 454 Are Logged Repeatedly in the Application Event Log

And there we see Message 2 – the exact error from our event log.

Resolution

The problem is that the local Group Policy database is corrupt. To fix it, we will need to re-create the local security database. This procedure is described in the article. The article doesn’t tell us what might have caused the problem in the first place. There could be any number of causes and finding one is going to take a bit more investigative work, but for now we’ll just worry about fixing the problem.

After re-creating the security database from template, per the instructions, I ran the gpupdate /force command. Hey presto! Group policy now applies correctly. This Application event log entry confirms it – Event ID 1704:

Automation

I discovered that this problem was more widespread than just one machine, so I set about looking to automate detection and resolution of the problem. Thankfully secedit.exe provides command line switches to do this:

http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/secedit_cmds.mspx?mfr=true

I decided to use this in a PowerShell script. The first thing to do was to find a way to detect whether the problem exists. Using Get-EventLog, I queried for the exact 1202 event and checked if any 1704 events had occurred since the 1202 event. If a 1704 event has occurred since the 1202 warning, I ignore it, because Group Policy has applied successfully.

Next, the script takes a backup of the secedit.sdb for rollback and proceeds to re-create the local group policy database file using the secedit command line.

In my tests, secedit.exe returned an error code 3, although the task appeared to complete successfully and the issue was resolved. I checked the secedit log file and found this entry at the end of the file:

"Task is completed. Some files in the configuration are not found on this system so security cannot be set/queried. It’s ok to ignore."

I can only assume that this is the reason for error code 3 being returned. The script deems error level 3, combined with this entry in the log to be a successful re-creation of the database file, otherwise it rolls back to the original secedit.sdb file.

I have provided the code below, which needs to be run locally on the machine. The script has only been tested on Windows XP. You could deploy this out via Group Policy, but it won’t be much good if the computers you are trying to target aren’t applying Group Policies! So, you’ll have to use a software distribution tool, psexec, or some other mechanism.

Warning: Re-creating the local security database file can reset permissions on a whole bunch of files and registry keys, so do this at your own peril.

#*=============================================
#* FUNCTION LISTINGS
#*=============================================

Function Exit-Script {
	Param($exitCode)
	Write-Log "Exiting Script."
	Exit $exitCode
}

# Function to write output to the host and to log file
Function Write-Log {
	[CmdletBinding(DefaultParametersetName="LogEntry")] 
    Param(
	[Parameter(ParameterSetName="LogEntry", Position = 0, Mandatory = $true, ValueFromPipeline = $true)][ValidateNotNullOrEmpty()]
    [String]$Message,
    [Parameter(Position = 1)]
    [String]$LogFile = "$env:temp\$(Split-Path $MyInvocation.ScriptName -Leaf).log",
	[Parameter(ParameterSetName="LogEntry", Position = 2)] [ValidateSet("Error", "Warning")]
	[string]$Type = "",	
	[Parameter(ParameterSetName="NewLine", Position=0)] 
	[switch]$NewLine	
    )		
	
	switch ($PsCmdlet.ParameterSetName) 
    { 
	    "NewLine" 	{ Write-Host ""; "" | Out-File -FilePath $LogFile -Append }
	    "LogEntry" 	{			
			
			$date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"	
			$psCallStack = Get-PSCallStack
			
			# Get the function name from where the Write-Log function was called
			If ($psCallStack[1].command -notmatch ".") { $function = $psCallStack[1].command } Else { $function = "Script" } 
			
			$logColumns = @("$date","$function","$($Type.ToUpper())","$message")
			[string]$logMessage = ""	
			
			# Build the message string to insert colon only where needed
			Foreach ($logColumn in $logColumns) { 
				If ($logColumn -ne "") {
					If ($logMessage -eq "" ) { 
						$logMessage = $logColumn
					}
					Else { 
						$logMessage += [string]::join("$logMessage"," : $logColumn")
					}
				}							
			}

			# Write message to the screen
			Switch ($type) {
				"Error" 	{ $logMessage += ": Line $($MyInvocation.ScriptLineNumber)"; Write-Host $logMessage -ForegroundColor Red }
				"Warning" 	{ Write-Host $logMessage -ForegroundColor Yellow }
				Default		{ Write-Host $logMessage }				
			}
			
			# Write message to the log file
		    Write-Output $logMessage | Out-File -FilePath $LogFile -Append
		}
	}
} #end function

# Function to start a CLI application and return the exit code
Function Start-CliApplication { 
    param ( [string]$application, [string]$arguments )
	
    # Build Startinfo and set options according to parameters
	$startInfo = new-object System.Diagnostics.ProcessStartInfo 
   	$startInfo.FileName = $application
	$startInfo.Arguments = $arguments
	$startInfo.WindowStyle = "Hidden"
	$startInfo.CreateNoWindow = $true
	$startInfo.UseShellExecute = $false
    	
	# Start the process
	$process = [System.Diagnostics.Process]::Start($startinfo)

	# Wait until the process finished
	Do {
 		If( -not $process.HasExited ) {
  			$process.Refresh()
		}
	} While( -not $process.WaitForExit(1000) )
	
	# Output the exitcode
	Write $process.exitcode
}

Function Backup-SecDB {
	If (Test-Path (Join-Path $env:SystemRoot "Security\Database\secedit.sdb")) {
		Write-Log "Backing up Security Database..."
		Rename-Item -Path (Join-Path $env:SystemRoot "Security\Database\secedit.sdb") -NewName "secedit.old" -Force
	}
}

Function Restore-SecDB {
	If (Test-Path (Join-Path $env:SystemRoot "Security\Database\secedit.old")) {
		Write-Log "Restoring Old Security Database..."
		Rename-Item -Path (Join-Path $env:SystemRoot "Security\Database\secedit.old") -NewName "secedit.sdb" -Force
	}
}

Function Reset-SecurityDatabase {
	# Reset the security database from template
	Write-Log "Regenerating Security Database..."
	$RegenerationResult = Start-CliApplication -Application "SECEDIT.EXE" -Arguments "/CONFIGURE /CFG `"$env:systemroot\Security\Templates\setup security.inf`" /DB `"$env:temp\secedit.sdb`" /OVERWRITE /LOG `"$env:temp\SeceditRegeneration.log`" /QUIET"
	If ($RegenerationResult -eq 0) {
        Write-Log "Regeneration of Security Database Successful" 
    }
    ElseIf ($RegenerationResult -eq 3) {
        If (Test-Path (Join-Path $env:temp "\SeceditRegeneration.log")) { 
            $seceditRegenerationLog = Get-Content (Join-Path $env:temp "\SeceditRegeneration.log")
            If ($seceditRegenerationLog -match "It's ok to ignore") {
                 Write-Log "Regeneration of Security Database Successful."
            }
            Else { Write-Log "Regeneration of Security Database Failed." -Type Error; Restore-SecDB; Exit-Script 1 } 
        }
        Else { Write-Log "Regeneration of Security Database Failed." -Type Error; Restore-SecDB; Exit-Script 1 } 
   	}
   	Else { Write-Log "Regeneration of Security Database Failed." -Type Error; Restore-SecDB; Exit-Script 1 } 

	Write-Log "Replacing Security Database..."
	Move-Item -Path (Join-Path $env:Temp "secedit.sdb") -Destination (Join-Path $env:SystemRoot Security\Database) -Force
}

Function Update-GroupPolicy {
	# Update Group Policy
	Write-Log "Updating Group Policy..."
	$gpupdateResult = Start-CliApplication "gpupdate" "/force"
	If ($gpUpdateResult -eq 0) {
		Write-Log "Group Policy Update Successful"
	}
	Else { Write-Log "Group Policy Update Failed" -Type Error; Exit-Script 1}
}

#*=============================================
#* END FUNCTION LISTINGS
#*=============================================

#*=============================================
#* SCRIPT BODY
#*=============================================

Write-Log "Searching Event Log for Security Database errors..."
$eventSceCliErrors = Get-EventLog Application -Newest 500 | Where { $_.eventID -eq "1202" -and $_.source -eq "SceCli" -and $_.message -match "0x4b8" } | Sort-Object -Property TimeGenerated -Descending | Select-Object -First 1
$eventSceCliSuccess = Get-EventLog Application -Newest 500 | Where { $_.eventID -eq "1704" -and $_.source -eq "SceCli" -and $_.message -match "successfully" } | Sort-Object -Property TimeGenerated -Descending | Select-Object -First 1

# If group policy has not been applied successfully since the last SceCli extended error, initiate remediation action. 
If ($eventSceCliErrors.TimeGenerated -gt $eventSceCliSuccess.TimeGenerated) {
	Write-Log "Group Policy has not been applied successfully since the last SceCLI Errors detected in the Event Log. Initiating Remediation Action..." -Type Warning
	
	# Backup the security database file
	Backup-SecDB
	
	# Regenerate the local security database from template
	Reset-SecurityDatabase
	
	# Update Group Policy
	Update-GroupPolicy
}
ElseIf ($eventSceCliErrors) {
	Write-Log "SceCLI Errors detected in the Event Log but Group Policy has successfully applied since the errors were generated.`nNo action necessary."
}
Else {
	Write-Log "No SceCLI Errors detected in the Event Log."
}

Write-Log "Script Complete."
Exit-Script 0

#*=============================================
#* END SCRIPT BODY
#*=============================================


This code is provided "as is" and without warranties as to performance or merchantability. The author and/or distributors of this code may have made statements about this code. Any such statements do not constitute warranties and shall not be relied on by the user in deciding whether to use this code.

This code is provided without any express or implied warranties whatsoever. Because of the diversity of conditions and hardware under which this code may be used, no warranty of fitness for a particular purpose is offered. The user is advised to test the code thoroughly before relying on it. The user must assume the entire risk of using the code.

Welcome to my blog!

Posted: March 9, 2011 in General

I have been meaning to set up a blog for some time and I have finally decided to do something about it!

My goal is to start sharing some of the things I am working on and anything I think might be useful to others. I expect to be blogging mostly about PowerShell, but I’m sure I’ll touch on different areas in doing so, such as Windows administration, VMware, Security and wherever else my job or interests take me!

Hopefully someone will read it! 🙂