Qualys VM v2 data connector and workbook
This commit is contained in:
Родитель
bb5ee1c469
Коммит
6730f9b3fc
Двоичный файл не отображается.
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"bindings": [
|
||||
{
|
||||
"type": "timerTrigger",
|
||||
"name": "Timer",
|
||||
"schedule": "0 */5 * * * *",
|
||||
"direction": "in"
|
||||
}
|
||||
],
|
||||
"disabled": false
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
<#
|
||||
Title: Qualys Vulnerability Management (VM) Host Detection Data Connector
|
||||
Language: PowerShell
|
||||
Version: 1.2
|
||||
Author(s): Microsoft
|
||||
Last Modified: 8/14/2020
|
||||
Comment: Added pagination support and flatten the data.
|
||||
|
||||
DESCRIPTION
|
||||
This Function App calls the Qualys Vulnerability Management (VM) API (https://www.qualys.com/docs/qualys-api-vmpc-user-guide.pdf) specifically for Host List Detection data (/api/2.0/fo/asset/host/vm/detection/).
|
||||
The response from the Qualys API is recieved in XML format. This function will parse the XML into JSON format, build the signature and authorization header needed to post the data
|
||||
to the Log Analytics workspace via the HTTP Data Connector API. The Function App will omit API responses that with an empty host list, which indicates there were no records for that
|
||||
time interval. Often, there are Hosts with numerous scan detections, which causes the record submitted to the Data Connector API to be truncated and improperly ingested, The Function App
|
||||
will also identify those records greater than the 32Kb limit per record and seperate them into individual records.
|
||||
#>
|
||||
|
||||
# Input bindings are passed in via param block
|
||||
param($Timer)
|
||||
|
||||
# Get the current Universal Time
|
||||
$currentUTCtime = (Get-Date).ToUniversalTime()
|
||||
|
||||
# The 'IsPastDue' property is 'true' when the current function invocation is later than was originally scheduled
|
||||
if ($Timer.IsPastDue) {
|
||||
Write-Host "PowerShell timer is running late!"
|
||||
}
|
||||
|
||||
# Define the Log Analytics Workspace ID and Key and Custom Table Name
|
||||
$CustomerId = $env:workspaceId
|
||||
$SharedKey = $env:workspaceKey
|
||||
$TimeStampField = "DateValue"
|
||||
$TableName = "QualysHostDetection"
|
||||
|
||||
# Build the headers for the Qualys API request
|
||||
$username = $env:apiUserName
|
||||
$password = $env:apiPassword
|
||||
$logAnalyticsUri = $env:logAnalyticsUri
|
||||
$hdrs = @{"X-Requested-With"="PowerShell"}
|
||||
$uri = $env:uri
|
||||
$filterParameters = $env:filterParameters
|
||||
$api = "/api/2.0/fo/asset/host/vm/detection?"
|
||||
$LOGGED = $BATCH = 0
|
||||
$param = @{'status'='New,Active,Fixed,Re-Opened'; 'action'='list'; 'show_results'=1; 'show_igs'=0}
|
||||
|
||||
# ISO:8601-compliant DateTime required.
|
||||
$time = $env:timeInterval
|
||||
# the $time will be reduced from the current UTC time to achive incremental pull.
|
||||
$vm_processed_before = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
$vm_processed_after = ([System.DateTime]::UtcNow.AddMinutes(-$($time))).ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||
|
||||
if ([string]::IsNullOrEmpty($logAnalyticsUri))
|
||||
{
|
||||
$logAnalyticsUri = "https://" + $CustomerId + ".ods.opinsights.azure.com"
|
||||
}
|
||||
|
||||
# Returning if the Log Analytics Uri is in incorrect format.
|
||||
# Sample format supported: https://" + $customerId + ".ods.opinsights.azure.com
|
||||
if($logAnalyticsUri -notmatch 'https:\/\/([\w\-]+)\.ods\.opinsights\.azure.([a-zA-Z\.]+)$')
|
||||
{
|
||||
throw "QualysVM: Invalid Log Analytics Uri."
|
||||
}
|
||||
|
||||
#check if the filterParameters are allowed or not
|
||||
$allParameters = ""
|
||||
$notAllowedParams = @("action","vm_processed_after", "vm_processed_before")
|
||||
if ($filterParameters){
|
||||
$filterParameters.split("&") | ForEach-Object{
|
||||
$k,$v = $_.split("=")
|
||||
if ($notAllowedParams.Contains($k)){
|
||||
Write-Host "$_ parameter is not allowed and not added in the request. Please remove it from the filterParameters."
|
||||
} else{
|
||||
Write-Host "adding filterParameters: $_"
|
||||
$param[$k] = $v
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach($i in $param.Keys){
|
||||
$allParameters += "${i}=$($param.Item($i))&"
|
||||
}
|
||||
# create a request URI
|
||||
$all_params = $allParameters+"vm_processed_after="+$vm_processed_after+"&vm_processed_before="+$vm_processed_before
|
||||
$request = ($uri + $api + $all_params)
|
||||
|
||||
# try creating a session to get the data from Qualys
|
||||
try {
|
||||
Write-Host "Trying to create a session"
|
||||
$base = [regex]::matches(($uri+ $api), '(https:\/\/[\w\.]+\/api\/\d\.\d\/fo)').captures.groups[1].value
|
||||
$body = "action=login&username=$($username)&password=$($password)"
|
||||
# Create a Logon Session variable
|
||||
Invoke-RestMethod -Headers $hdrs -Uri "$base/session/" -Method Post -Body $body -SessionVariable LogonSession
|
||||
|
||||
} catch{
|
||||
$exp = $_.Exception
|
||||
$expStatusCode = $exp.Response.StatusCode.value__
|
||||
if($expStatusCode -eq 401){
|
||||
Write-Host "APIStatusCode:$expStatusCode`nAPIStatusMessage:$exp.Message `nPlease verify the API credentials. Not able to create session. `nError @ line #$line. `nI'm exiting now!!"
|
||||
} elseif (-not ($expStatusCode -eq 200)){
|
||||
Write-Host "APIStatusCode:$expStatusCode `nAPIStatusMessage:$exp. `nMessage Not able to create a session. `nError @ line #$line. `nI'm exiting now!!"
|
||||
}
|
||||
Invoke-WebRequest -Headers $hdrs -Uri "$($base)/session/" -Method Post -Body "action=logout" -WebSession $LogonSession
|
||||
Exit
|
||||
}
|
||||
# print the request details
|
||||
Write-Host "Session creation is successfull `nUsing API Server: $uri `nUsing Host Detection API: $api `nUsing Username: $username `nUsing Parameters : $all_params, `nTable name: $TableName"
|
||||
|
||||
#===================================== Function Definitions =====================================#
|
||||
|
||||
# Function to build the Authorization signature for the Log Analytics Data Connector API
|
||||
Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource)
|
||||
{
|
||||
$xHeaders = "x-ms-date:" + $date
|
||||
$stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource
|
||||
|
||||
$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
|
||||
$keyBytes = [Convert]::FromBase64String($sharedKey)
|
||||
|
||||
$sha256 = New-Object System.Security.Cryptography.HMACSHA256
|
||||
$sha256.Key = $keyBytes
|
||||
$calculatedHash = $sha256.ComputeHash($bytesToHash)
|
||||
$encodedHash = [Convert]::ToBase64String($calculatedHash)
|
||||
$authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
|
||||
|
||||
# Dispose SHA256 from heap before return
|
||||
$sha256.Dispose()
|
||||
|
||||
return $authorization
|
||||
} # Build-Signature
|
||||
|
||||
# Function to create and invoke an API POST request to the Log Analytics Data Connector API
|
||||
Function Post-LogAnalyticsData($customerId, $sharedKey, $body, $logType)
|
||||
{
|
||||
$method = "POST"
|
||||
$contentType = "application/json"
|
||||
$resource = "/api/logs"
|
||||
$rfc1123date = [DateTime]::UtcNow.ToString("r")
|
||||
$contentLength = $body.Length
|
||||
$signature = Build-Signature `
|
||||
-customerId $customerId `
|
||||
-sharedKey $sharedKey `
|
||||
-date $rfc1123date `
|
||||
-contentLength $contentLength `
|
||||
-method $method `
|
||||
-contentType $contentType `
|
||||
-resource $resource
|
||||
$uri = $logAnalyticsUri + $resource + "?api-version=2016-04-01"
|
||||
|
||||
$headers = @{
|
||||
"Authorization" = $signature;
|
||||
"Log-Type" = $logType;
|
||||
"x-ms-date" = $rfc1123date;
|
||||
"time-generated-field" = $TimeStampField;
|
||||
}
|
||||
|
||||
$response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
|
||||
return $response.StatusCode
|
||||
|
||||
} # Post-LogAnalyticsData
|
||||
|
||||
# Iterate through each detection recieved from the API call and assign the variables (Column Names in LA) to each XML variable
|
||||
Function Parse-and-Send($qualysResponse){
|
||||
$detections = @()
|
||||
$results = "NA"
|
||||
#iterate over the HOST LIST AND DETECTION LIST to have gerenralised detections
|
||||
$qualysResponse.HOST_LIST_VM_DETECTION_OUTPUT.RESPONSE.HOST_LIST.HOST | ForEach-Object {
|
||||
$hostObject = New-Object -TypeName PSObject
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "HostId" -Value $_.ID
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "IpAddress" -Value $_.IP
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "TrackingMethod" -Value $_.TRACKING_METHOD
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "OperatingSystem" -Value $_.OS."#cdata-section"
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "DnsName" -Value $_.DNS."#cdata-section"
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "NetBios" -Value $_.NETBIOS."#cdata-section"
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "QGHostId" -Value $_.QG_HOSTID."#cdata-section"
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "LastScanDateTime" -Value $_.LAST_SCAN_DATETIME
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "LastVMScannedDateTime" -Value $_.LAST_VM_SCANNED_DATE
|
||||
Add-Member -InputObject $hostObject -MemberType NoteProperty -Name "LastVMAuthScannedDateTime" -Value $_.LAST_VM_AUTH_SCANNED_DATE
|
||||
Write-Output "Adding data for Host id = $($_.ID)"
|
||||
|
||||
foreach($detection in $_.DETECTION_LIST.DETECTION){
|
||||
$detectionObject = $hostObject.PsObject.Copy()
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "QID" -Value $detection.QID
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "SSL" -Value $detection.SSL
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Type" -Value $detection.TYPE
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Status" -Value $detection.STATUS
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Ignored" -Value $detection.IS_IGNORED
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Severity" -Value $detection.SEVERITY
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Disabled" -Value $detection.IS_DISABLED
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "LastFixed" -Value $detection.LAST_FIXED_DATETIME
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "LastFound" -Value $detection.LAST_FOUND_DATETIME
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "TimesFound" -Value $detection.TIMES_FOUND
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "FirstFound" -Value $detection.FIRST_FOUND_DATETIME
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "LastUpdate" -Value $detection.LAST_UPDATE_DATETIME
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "LastProcessed" -Value $detection.LAST_PROCESSED_DATE
|
||||
$results = $detection.RESULTS.'#cdata-section'
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Result_column_count" -Value 1
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Results_0" -Value $results
|
||||
# if the RESULTS field has data more than 30KB chunk it
|
||||
if ($results){
|
||||
[bool] $do_collect = $true
|
||||
$results_array = @()
|
||||
Do{
|
||||
$kbyte = ([System.Text.Encoding]::UTF8.GetBytes($results)).Count/1024
|
||||
if ($kbyte -gt 30){
|
||||
$regex = [regex] "\b"
|
||||
$r1, $r2 = $regex.split($results, 2, 30000)
|
||||
$results_array += $r1
|
||||
$results = $r2
|
||||
}
|
||||
else{
|
||||
if ($results_array){
|
||||
$results_array += $results
|
||||
$result_column_count = $results_array.Length
|
||||
$detectionObject.Result_column_count = $result_column_count
|
||||
for ($i = 0; $i -lt $result_column_count; $i++){
|
||||
$result_column = "Results_$i"
|
||||
if ([bool]($detectionObject.PSobject.Properties.name -match $result_column)){
|
||||
$detectionObject.$result_column = $results_array[$i]
|
||||
} else{
|
||||
Add-Member -InputObject $detectionObject -MemberType NoteProperty -Name "Results_$i" -Value $results_array[$i]
|
||||
} # end of if-else for checking and populate if the detectionObject has the member or not
|
||||
} # end of for loop to add chunked results in detectionObject's member columns
|
||||
} # end of if the $results_array is populated with chunked results data
|
||||
$do_collect = $false
|
||||
}
|
||||
}while($do_collect) # this do-while is used to collect the chunked Results field in results_array. As per the HTTP Data Collector API, the field value should not exide 32KB data limit.
|
||||
}# end of if where we check if the Results in null or not
|
||||
|
||||
$detections += $detectionObject
|
||||
#create a array list of detection per Host Id
|
||||
$jsonPayload = $detections | ConvertTo-Json -Compress -Depth 3
|
||||
$mbyte = ([System.Text.Encoding]::UTF8.GetBytes($jsonPayload)).Count/1024/1024
|
||||
# if the detections object has payload more than 27MB or less than equal to 30MB we will POST the payload and rest will be POSTED out of the detectionObject loop.
|
||||
if (($mbytes -gt 27) -and ($mbytes -le 30)){
|
||||
$qidLength = [int] $detections.length
|
||||
$id = $hostObject.HostId
|
||||
$responseCode = Post-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonPayload)) -logType $TableName
|
||||
if ($responseCode -ne 200){
|
||||
Write-Host "ERROR: Log Analytics POST, Status Code: $responseCode. Host Id: $id with QID count: $qidLength, Not able to Log."
|
||||
}else {
|
||||
$LOGGED += $qidLength
|
||||
Write-Host "SUCCESS: Log Analytics POST, Status Code: $responseCode. Host Id: $id with QID count: $qidLength, logged successfully. DETECTIONS LOGGED: $qidLength, in batch: $BATCH"
|
||||
}
|
||||
$detections = @()
|
||||
$responseCode = 0
|
||||
}
|
||||
# reinitialise the object to have the next host
|
||||
$detectionObject = New-Object -TypeName PSObject
|
||||
}# end of detectionObject for loop
|
||||
|
||||
# if the detections object is greater than 0MB and less than or equal to 30MB we will POST the payload from here
|
||||
if ($detections.Count -gt 0) {
|
||||
# we probably did not flush at point A. So we need to POST to Sentinel API now.
|
||||
$jsonPayload = $detections | ConvertTo-Json -Compress -Depth 3
|
||||
$id = $hostObject.HostId
|
||||
$qidLength = [int] $detections.Length
|
||||
$responseCode = Post-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($jsonPayload)) -logType $TableName
|
||||
|
||||
if ($responseCode -ne 200){
|
||||
Write-Host "ERROR: Log Analytics POST, Status Code: $responseCode. Host Id: $id with QID count: $qidLength, Not able to Log."
|
||||
}else {
|
||||
$LOGGED += $qidLength
|
||||
Write-Host "SUCCESS: Log Analytics POST, Status Code: $responseCode. Host Id: $id with QID count: $qidLength, logged successfully. DETECTIONS LOGGED: $qidLength, in batch: $BATCH"
|
||||
}
|
||||
}
|
||||
# reinitialise the object to have the correct count of detections
|
||||
[int] $script:TOTAL_LOGGED += [int] $LOGGED
|
||||
$LOGGED = 0
|
||||
$responseCode = 0
|
||||
$detections = @()
|
||||
}# end of hostObject for loop
|
||||
} # end of Parse-and-Send Function
|
||||
|
||||
#===================================== main =====================================#
|
||||
[bool] $keep_running = $true
|
||||
|
||||
Do {
|
||||
try {
|
||||
Write-Host "Making Request: $request"
|
||||
$response = Invoke-RestMethod -Headers $hdrs -Uri $request -WebSession $LogonSession
|
||||
|
||||
if ($response.HOST_LIST_VM_DETECTION_OUTPUT.RESPONSE.HOST_LIST -eq $null) {
|
||||
Write-Output "No new results found for this interval. Exiting..."
|
||||
$keep_running = $false
|
||||
} else {
|
||||
$request = ""
|
||||
# provide the response for parsing to Parse-and-Send Function
|
||||
Parse-and-Send $response
|
||||
$request = $response.selectnodes("//WARNING").URL."#cdata-section"
|
||||
if($request){
|
||||
Write-Host "Making Paginated Request."
|
||||
$BATCH += 1
|
||||
}else{
|
||||
Write-Output "All data fetched!"
|
||||
[bool] $keep_running = $false
|
||||
}# end of pegination if
|
||||
}
|
||||
} catch{
|
||||
$exp = $_.Exception
|
||||
$expStatusCode = $exp.Response.StatusCode.value__
|
||||
$line = $_.InvocationInfo.ScriptLineNumber
|
||||
if (-not ($expStatusCode -eq 200)){
|
||||
Write-Host "APIStatusCode:$expStatusCode `nAPIStatusMessage:$exp.Message. `nError @ line #$line. `nI'm exiting!"
|
||||
} elseif ($expStatusCode -eq 409){
|
||||
Write-Host "API concurrency limit reached.`nError @ line #$line. `nI'm exiting!"
|
||||
}
|
||||
Invoke-WebRequest -Headers $hdrs -Uri "$($base)/session/" -Method Post -Body "action=logout" -WebSession $LogonSession
|
||||
Exit
|
||||
}
|
||||
} while($keep_running) # end of main while loop
|
||||
|
||||
# dispose of the session
|
||||
Invoke-WebRequest -Headers $hdrs -Uri "$($base)/session/" -Method Post -Body "action=logout" -WebSession $LogonSession
|
||||
Write-Host "Qualys Host Detection session ended `nTOTAL DETECTIONS LOGGED: $script:TOTAL_LOGGED `nPowerShell timer trigger function ran! TIME: $currentUTCtime"
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"id": "QualysVulnerabilityManagementV2",
|
||||
"title": "Qualys Vulnerability Management",
|
||||
"publisher": "Qualys",
|
||||
"descriptionMarkdown": "The [Qualys Vulnerability Management (VM)](https://www.qualys.com/apps/vulnerability-management/) data connector provides the capability to ingest vulnerability host detection data into Azure Sentinel through the Qualys API. The connector provides visibility into host detection data from vulerability scans. This connector provides Azure Sentinel the capability to view dashboards, create custom alerts, and improve investigation ",
|
||||
"graphQueries": [
|
||||
{
|
||||
"metricName": "Total data received",
|
||||
"legend": "QualysHostDetection_CL",
|
||||
"baseQuery": "QualysHostDetection_CL"
|
||||
}
|
||||
],
|
||||
"sampleQueries": [
|
||||
{
|
||||
"description" : "Top 10 Vulerabilities detected",
|
||||
"query": "QualysHostDetection_CL\n | extend Vulnerability = tostring(Results_0_s)\n | summarize count() by Vulnerability\n | top 10 by count_"
|
||||
}
|
||||
],
|
||||
"dataTypes": [
|
||||
{
|
||||
"name": "QualysHostDetection_CL",
|
||||
"lastDataReceivedQuery": "QualysHostDetection_CL\n | summarize Time = max(TimeGenerated)\n | where isnotempty(Time)"
|
||||
}
|
||||
],
|
||||
"connectivityCriterias": [
|
||||
{
|
||||
"type": "IsConnectedQuery",
|
||||
"value": [
|
||||
"QualysHostDetection_CL\n | summarize LastLogReceived = max(TimeGenerated)\n | project IsConnected = LastLogReceived > ago(30d)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"availability": {
|
||||
"status": 1,
|
||||
"isPreview": true
|
||||
},
|
||||
"permissions": {
|
||||
"resourceProvider": [
|
||||
{
|
||||
"provider": "Microsoft.OperationalInsights/workspaces",
|
||||
"permissionsDisplayText": "read and write permissions on the workspace are required.",
|
||||
"providerDisplayName": "Workspace",
|
||||
"scope": "Workspace",
|
||||
"requiredPermissions": {
|
||||
"write": true,
|
||||
"read": true,
|
||||
"delete": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"provider": "Microsoft.OperationalInsights/workspaces/sharedKeys",
|
||||
"permissionsDisplayText": "read permissions to shared keys for the workspace are required. [See the documentation to learn more about workspace keys](https://docs.microsoft.com/azure/azure-monitor/platform/agent-windows#obtain-workspace-id-and-key).",
|
||||
"providerDisplayName": "Keys",
|
||||
"scope": "Workspace",
|
||||
"requiredPermissions": {
|
||||
"action": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"customs": [
|
||||
{
|
||||
"name": "Microsoft.Web/sites permissions",
|
||||
"description": "Read and write permissions to Azure Functions to create a Function App is required. [See the documentation to learn more about Azure Functions](https://docs.microsoft.com/azure/azure-functions/)."
|
||||
},
|
||||
{
|
||||
"name": "Qualys API Key",
|
||||
"description": "A Qualys VM API username and password is required. [See the documentation to learn more about Qualys VM API](https://www.qualys.com/docs/qualys-api-vmpc-user-guide.pdf)."
|
||||
}
|
||||
]
|
||||
},
|
||||
"instructionSteps": [
|
||||
{
|
||||
"title": "",
|
||||
"description": ">**NOTE:** This connector uses Azure Functions to connect to Qualys VM to pull its logs into Azure Sentinel. This might result in additional data ingestion costs. Check the [Azure Functions pricing page](https://azure.microsoft.com/pricing/details/functions/) for details."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": ">**(Optional Step)** Securely store workspace and API authorization key(s) or token(s) in Azure Key Vault. Azure Key Vault provides a secure mechanism to store and retrieve key values. [Follow these instructions](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) to use Azure Key Vault with an Azure Function App."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**STEP 1 - Configuration steps for the Qualys VM API**\n\n1. Log into the Qualys Vulnerability Management console with an administrator account, select the **Users** tab and the **Users** subtab. \n2. Click on the **New** drop-down menu and select **Users..**\n3. Create a username and password for the API account. \n4. In the **User Roles** tab, ensure the account role is set to **Manager** and access is allowed to **GUI** and **API**\n4. Log out of the administrator account and log into the console with the new API credentials for validation, then log out of the API account. \n5. Log back into the console using an administrator account and modify the API accounts User Roles, removing access to **GUI**. \n6. Save all changes."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**STEP 2 - Choose ONE from the following two deployment options to deploy the connector and the associated Azure Function**\n\n>**IMPORTANT:** Before deploying the Qualys VM connector, have the Workspace ID and Workspace Primary Key (can be copied from the following), as well as the Qualys VM API Authorization Key(s), readily available.",
|
||||
"instructions":[
|
||||
{
|
||||
"parameters": {
|
||||
"fillWith": [
|
||||
"WorkspaceId"
|
||||
],
|
||||
"label": "Workspace ID"
|
||||
},
|
||||
"type": "CopyableLabel"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fillWith": [
|
||||
"PrimaryKey"
|
||||
],
|
||||
"label": "Primary Key"
|
||||
},
|
||||
"type": "CopyableLabel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Option 1 - Azure Resource Manager (ARM) Template",
|
||||
"description": "Use this method for automated deployment of the Qualys VM connector using an ARM Tempate.\n\n1. Click the **Deploy to Azure** button below. \n\n\t[![Deploy To Azure](https://aka.ms/deploytoazurebutton)](https://aka.ms/sentinel-QualysVM-azuredeployV2)\n2. Select the preferred **Subscription**, **Resource Group** and **Location**. \n3. Enter the **Workspace ID**, **Workspace Key**, **API Username**, **API Password** , update the **URI**, and any additional URI **Filter Parameters** (each filter should be separated by an \"&\" symbol, no spaces.) \n> - Enter the URI that corresponds to your region. The complete list of API Server URLs can be [found here](https://www.qualys.com/docs/qualys-api-vmpc-user-guide.pdf#G4.735348) -- There is no need to add a time suffix to the URI, the Function App will dynamically append the Time Value to the URI in the proper format. \n - The default **Time Interval** is set to pull the last five (5) minutes of data. If the time interval needs to be modified, it is recommended to change the Function App Timer Trigger accordingly (in the function.json file, post deployment) to prevent overlapping data ingestion. \n> - Note: If using Azure Key Vault secrets for any of the values above, use the`@Microsoft.KeyVault(SecretUri={Security Identifier})`schema in place of the string values. Refer to [Key Vault references documentation](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) for further details. \n4. Mark the checkbox labeled **I agree to the terms and conditions stated above**. \n5. Click **Purchase** to deploy."
|
||||
},
|
||||
{
|
||||
"title": "Option 2 - Manual Deployment of Azure Functions",
|
||||
"description": "Use the following step-by-step instructions to deploy the Quayls VM connector manually with Azure Functions."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**1. Create a Function App**\n\n1. From the Azure Portal, navigate to [Function App](https://portal.azure.com/#blade/HubsExtension/BrowseResource/resourceType/Microsoft.Web%2Fsites/kind/functionapp), and select **+ Add**.\n2. In the **Basics** tab, ensure Runtime stack is set to **Powershell Core**. \n3. In the **Hosting** tab, ensure the **Consumption (Serverless)** plan type is selected.\n4. Make other preferrable configuration changes, if needed, then click **Create**."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**2. Import Function App Code**\n\n1. In the newly created Function App, select **Functions** on the left pane and click **+ New Function**.\n2. Select **Timer Trigger**.\n3. Enter a unique Function **Name** and leave the default cron schedule of every 5 minutes, then click **Create**.\n5. Click on **Code + Test** on the left pane. \n6. Copy the [Function App Code](https://aka.ms/sentinelqualysvmazurefunctioncode) and paste into the Function App `run.ps1` editor.\n7. Click **Save**."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**3. Configure the Function App**\n\n1. In the Function App, select the Function App Name and select **Configuration**.\n2. In the **Application settings** tab, select **+ New application setting**.\n3. Add each of the following eight (8) application settings individually, with their respective string values (case-sensitive): \n\t\tapiUsername\n\t\tapiPassword\n\t\tworkspaceID\n\t\tworkspaceKey\n\t\turi\n\t\tfilterParameters\n\t\ttimeInterval\n\t\tlogAnalyticsUri (optional)\n> - Enter the URI that corresponds to your region. The complete list of API Server URLs can be [found here](https://www.qualys.com/docs/qualys-api-vmpc-user-guide.pdf#G4.735348). The `uri` value must follow the following schema: `https://<API Server>/api/2.0/fo/asset/host/vm/detection/?action=list&vm_processed_after=` -- There is no need to add a time suffix to the URI, the Function App will dynamically append the Time Value to the URI in the proper format.\n> - Add any additional filter parameters, for the `filterParameters` variable, that need to be appended to the URI. Each parameter should be seperated by an \"&\" symbol and should not include any spaces.\n> - Set the `timeInterval` (in minutes) to the value of `5` to correspond to the Timer Trigger of every `5` minutes. If the time interval needs to be modified, it is recommended to change the Function App Timer Trigger accordingly to prevent overlapping data ingestion.\n> - Note: If using Azure Key Vault, use the`@Microsoft.KeyVault(SecretUri={Security Identifier})`schema in place of the string values. Refer to [Key Vault references documentation](https://docs.microsoft.com/azure/app-service/app-service-key-vault-references) for further details.\n> - Use logAnalyticsUri to override the log analytics API endpoint for dedicated cloud. For example, for public cloud, leave the value empty; for Azure GovUS cloud environment, specify the value in the following format: `https://<CustomerId>.ods.opinsights.azure.us`.\n4. Once all application settings have been entered, click **Save**."
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": "**4. Configure the host.json**.\n\nDue to the potentially large amount of Qualys host detection data being ingested, it can cause the execution time to surpass the default Function App timeout of five (5) minutes. Increase the default timeout duration to the maximum of ten (10) minutes, under the Consumption Plan, to allow more time for the Function App to execute.\n\n1. In the Function App, select the Function App Name and select the **App Service Editor** blade.\n2. Click **Go** to open the editor, then select the **host.json** file under the **wwwroot** directory.\n3. Add the line `\"functionTimeout\": \"00:10:00\",` above the `managedDependancy` line \n4. Ensure **SAVED** appears on the top right corner of the editor, then exit the editor.\n\n> NOTE: If a longer timeout duration is required, consider upgrading to an [App Service Plan](https://docs.microsoft.com/azure/azure-functions/functions-scale#timeout)"
|
||||
},
|
||||
{
|
||||
"title": "",
|
||||
"description": ">**NOTE:** This connector has been updated, if you have previously deployed an earlier version, and want to update, please delete the existing Qualys VM Azure Function before redeploying this version."
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
{
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"FunctionName": {
|
||||
"defaultValue": "QualysVM",
|
||||
"minLength": 1,
|
||||
"maxLength": 11,
|
||||
"type": "string"
|
||||
},
|
||||
"WorkspaceID": {
|
||||
"type": "string",
|
||||
"defaultValue": "<workspaceID>"
|
||||
},
|
||||
"WorkspaceKey": {
|
||||
"type": "string",
|
||||
"defaultValue": "<workspaceKey>"
|
||||
},
|
||||
"APIUsername": {
|
||||
"type": "string",
|
||||
"defaultValue": "<apiUsername>"
|
||||
},
|
||||
"APIPassword": {
|
||||
"type": "string",
|
||||
"defaultValue": "<apiPassword>"
|
||||
},
|
||||
"Uri": {
|
||||
"type": "string",
|
||||
"defaultValue": "https://<API Server URL>"
|
||||
},
|
||||
"FilterParameters": {
|
||||
"type": "string",
|
||||
"defaultValue": "&truncation_limit=50"
|
||||
},
|
||||
"TimeInterval": {
|
||||
"type": "string",
|
||||
"defaultValue": "5"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"FunctionName": "[concat(toLower(parameters('FunctionName')), uniqueString(resourceGroup().id))]",
|
||||
"StorageSuffix":"[environment().suffixes.storage]",
|
||||
"LogAnaltyicsUri":"[replace(environment().portal, 'https://portal', concat('https://', toLower(parameters('WorkspaceID')), '.ods.opinsights'))]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Insights/components",
|
||||
"apiVersion": "2015-05-01",
|
||||
"name": "[variables('FunctionName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"kind": "web",
|
||||
"properties": {
|
||||
"Application_Type": "web",
|
||||
"ApplicationId": "[variables('FunctionName')]"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[tolower(variables('FunctionName'))]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Standard_LRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"kind": "StorageV2",
|
||||
"properties": {
|
||||
"networkAcls": {
|
||||
"bypass": "AzureServices",
|
||||
"virtualNetworkRules": [
|
||||
],
|
||||
"ipRules": [
|
||||
],
|
||||
"defaultAction": "Allow"
|
||||
},
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"encryption": {
|
||||
"services": {
|
||||
"file": {
|
||||
"keyType": "Account",
|
||||
"enabled": true
|
||||
},
|
||||
"blob": {
|
||||
"keyType": "Account",
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"keySource": "Microsoft.Storage"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/serverfarms",
|
||||
"apiVersion": "2018-02-01",
|
||||
"name": "[variables('FunctionName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"sku": {
|
||||
"name": "Y1",
|
||||
"tier": "Dynamic"
|
||||
},
|
||||
"kind": "functionapp",
|
||||
"properties": {
|
||||
"name": "[variables('FunctionName')]",
|
||||
"workerSize": "0",
|
||||
"workerSizeId": "0",
|
||||
"numberOfWorkers": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[concat(variables('FunctionName'), '/default')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]"
|
||||
],
|
||||
"sku": {
|
||||
"name": "Standard_LRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"properties": {
|
||||
"cors": {
|
||||
"corsRules": [
|
||||
]
|
||||
},
|
||||
"deleteRetentionPolicy": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/fileServices",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[concat(variables('FunctionName'), '/default')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]"
|
||||
],
|
||||
"sku": {
|
||||
"name": "Standard_LRS",
|
||||
"tier": "Standard"
|
||||
},
|
||||
"properties": {
|
||||
"cors": {
|
||||
"corsRules": [
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Web/sites",
|
||||
"apiVersion": "2018-11-01",
|
||||
"name": "[variables('FunctionName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', tolower(variables('FunctionName')))]",
|
||||
"[resourceId('Microsoft.Web/serverfarms', variables('FunctionName'))]",
|
||||
"[resourceId('Microsoft.Insights/components', variables('FunctionName'))]"
|
||||
],
|
||||
"kind": "functionapp",
|
||||
"identity": {
|
||||
"type": "SystemAssigned"
|
||||
},
|
||||
"properties": {
|
||||
"name": "[variables('FunctionName')]",
|
||||
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('FunctionName'))]",
|
||||
"httpsOnly": true,
|
||||
"clientAffinityEnabled": true,
|
||||
"alwaysOn": true
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"apiVersion": "2018-11-01",
|
||||
"type": "config",
|
||||
"name": "appsettings",
|
||||
"dependsOn": [
|
||||
"[concat('Microsoft.Web/sites/', variables('FunctionName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"FUNCTIONS_EXTENSION_VERSION": "~3",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "powershell",
|
||||
"APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.insights/components', variables('FunctionName')), '2015-05-01').InstrumentationKey]",
|
||||
"APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('microsoft.insights/components', variables('FunctionName')), '2015-05-01').ConnectionString]",
|
||||
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', toLower(variables('FunctionName')),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', toLower(variables('FunctionName'))), '2019-06-01').keys[0].value, ';EndpointSuffix=',toLower(variables('StorageSuffix')))]",
|
||||
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=', toLower(variables('FunctionName')),';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', toLower(variables('FunctionName'))), '2019-06-01').keys[0].value, ';EndpointSuffix=',toLower(variables('StorageSuffix')))]",
|
||||
"WEBSITE_CONTENTSHARE": "[toLower(variables('FunctionName'))]",
|
||||
"workspaceID": "[parameters('WorkspaceID')]",
|
||||
"workspaceKey": "[parameters('WorkspaceKey')]",
|
||||
"apiUsername": "[parameters('APIUsername')]",
|
||||
"apiPassword": "[parameters('APIPassword')]",
|
||||
"uri": "[parameters('Uri')]",
|
||||
"filterParameters": "[parameters('FilterParameters')]",
|
||||
"timeInterval": "[parameters('TimeInterval')]",
|
||||
"logAnalyticsUri": "[variables('LogAnaltyicsUri')]",
|
||||
"WEBSITE_RUN_FROM_PACKAGE": "https://aka.ms/sentinel-QualysVM-functionappV2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[concat(variables('FunctionName'), '/default/azure-webjobs-hosts')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('FunctionName'), 'default')]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('FunctionName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"publicAccess": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[concat(variables('FunctionName'), '/default/azure-webjobs-secrets')]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('FunctionName'), 'default')]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('FunctionName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"publicAccess": "None"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts/fileServices/shares",
|
||||
"apiVersion": "2019-06-01",
|
||||
"name": "[concat(variables('FunctionName'), '/default/', tolower(variables('FunctionName')))]",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.Storage/storageAccounts/fileServices', variables('FunctionName'), 'default')]",
|
||||
"[resourceId('Microsoft.Storage/storageAccounts', variables('FunctionName'))]"
|
||||
],
|
||||
"properties": {
|
||||
"shareQuota": 5120
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "2.0",
|
||||
"functionTimeout": "00:10:00",
|
||||
"managedDependency": {
|
||||
"Enabled": true
|
||||
},
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[1.*, 2.0.0)"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
# Azure Functions profile.ps1
|
||||
#
|
||||
# This profile.ps1 will get executed every "cold start" of your Function App.
|
||||
# "cold start" occurs when:
|
||||
#
|
||||
# * A Function App starts up for the very first time
|
||||
# * A Function App starts up after being de-allocated due to inactivity
|
||||
#
|
||||
# You can define helper functions, run commands, or specify environment variables
|
||||
# NOTE: any variables defined that are not environment variables will get reset after the first execution
|
||||
# Authenticate with Azure PowerShell using MSI.
|
||||
# Remove this if you are not planning on using MSI or Azure PowerShell.
|
||||
|
||||
if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) {
|
||||
Connect-AzAccount -Identity
|
||||
}
|
||||
|
||||
# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell.
|
||||
# Enable-AzureRmAlias
|
||||
# You can also define functions or aliases that can be referenced in any of your PowerShell functions.
|
|
@ -0,0 +1,7 @@
|
|||
# This file enables modules to be automatically managed by the Functions service.
|
||||
# See https://aka.ms/functionsmanageddependency for additional information.
|
||||
#
|
||||
@{
|
||||
# For latest supported version, go to 'https://www.powershellgallery.com/packages/Az'.
|
||||
'Az' = '4.*'
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -818,6 +818,19 @@
|
|||
"subtitle": "",
|
||||
"provider": "Qualys"
|
||||
},
|
||||
{
|
||||
"workbookKey": "QualysVMV2Workbook",
|
||||
"logoFileName": "qualys_logo.svg",
|
||||
"description": "Gain insight into Qualys Vulnerability Management by analyzing, collecting and correlating vulnerability data.\nThis workbook provides visibility into vulnerabilities detected from vulnerability scans",
|
||||
"dataTypesDependencies": ["QualysHostDetection_CL"],
|
||||
"dataConnectorsDependencies": [ "QualysVulnerabilityManagementV2" ],
|
||||
"previewImagesFileNames": [ "QualysVMWhite.png", "QualysVMBlack.png" ],
|
||||
"version": "1.0",
|
||||
"title": "Qualys Vulnerability Management",
|
||||
"templateRelativePath": "QualysVMv2.json",
|
||||
"subtitle": "",
|
||||
"provider": "Qualys"
|
||||
},
|
||||
{
|
||||
"workbookKey": "GitHubSecurityWorkbook",
|
||||
"logoFileName": "GitHub.svg",
|
||||
|
|
Загрузка…
Ссылка в новой задаче