We’re all well-acquainted with the concept of security incidents, and it’s become a matter of “when” rather than “if” nowadays. In the past, we used to discuss our development life cycle, but gradually we realized the need to bring the operations teams closer. This led to the creation of DevOps teams, a well-known and widely used concept today. However, this approach has been outdated for quite some time within organizations. For an effective program or project, security must be incorporated as early as possible in their solutions.
Numerous solutions can help organizations integrate security from the beginning by involving security teams earlier in the project. For instance, Securemation offers the “Secure by Design” service, which enables organizations to identify threats during the design phase of the project. Nevertheless, this post will primarily focus on the development phase of the project and how security can lead to cost avoidance and reduction.
The traditional DevOps framework involves developing the solution first and conducting security tests at the end. However, this approach poses a problem: the security team may discover security threats deeply embedded in the core of your solution. If these security threats are tied to core functionalities, the entire solution might need to be redeveloped from scratch, significantly escalating project costs. DevSecOps, on the other hand, ensures that security tests and feedback are provided continuously, enabling the identification of security concerns at the earliest stage possible and responding proactively.
The DevSecOps approach can be utilized to mitigate threats in technology, people, and processes. This post will provide an in-depth analysis of how organizations using Microsoft Azure DevOps can apply this framework in their pipelines. To offer early feedback to the development team, we will leverage the following solutions:
• Software Composition Analysis (SCA): At this stage, we will ensure that the libraries we use in our project are secure and licence compliant (e.g., identifying vulnerable library versions or non-commercial libraries used in commercial applications).
• Static Application Security Test (SAST): During this stage, we will ensure that the code at rest in your repository does not contain any security vulnerabilities (e.g., authentication secrets in code or vulnerabilities introduced by poor code standards).
• Dynamic Application Security Test (DAST): In this stage, we will ensure that the code running on a server, while “alive,” does not have any security vulnerabilities.
SCA
For this example, we will use Mend Bolt to perform the analysis of the libraries and deliver a report in the build pipeline to the developers’ team.
The following code will allow you to add a task to perform the SCA analysis:
- stage: SAST
displayName: SAST stage
jobs:
- job: SCA
steps:
- task: WhiteSource@21
inputs:
cwd: '$(System.DefaultWorkingDirectory)'
Once the pipeline is finalised, you will have a report similar to the following:
SAST
The SAST stage will leverage SonarQube that has a community version that can be used in your environment. We will not cover how to setup your server in this post, and I will assume you already have it up and linked with you Microsoft Azure DevOps instance. The following code will add another task to your build pipeline and submit your code to the analysis process of SonarQube.
- stage: SAST
displayName: SAST stage
- job: SAST
steps:
- task: SonarQubePrepare@5
inputs:
SonarQube: 'Server connection'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'Project Key'
cliSources: '.'- task:
SonarQubeAnalyze@5- task:
SonarQubePublish@5inputs:
pollingTimeoutSec: '300'
Once the pipeline is finalised, you will have a report similar to the following:
DAST
The final stage of the pipeline DevSecOps approach will be adding the OWASP ZAP container to perform dynamic tests once your application is deployed and is running. The following code will add the DAST task to your release pipeline:
- Install Docker (Docker CLI installer)
- OWASP ZAP Image (Bash Task)
# Download OWASP ZAP v2.12.0
echo 'Get OWASP ZAP v2.12.0'
docker pull owasp/zap2docker-stable:2.12.0 - OWASP ZAP Full Scan (Bash Task)
# Full Scan
echo 'Full Scan https://YOUR_APPLICATION.com.au/'
chmod -R 777 ./
docker run --rm -v $(System.DefaultWorkingDirectory):/zap/wrk/:rw -t owasp/zap2docker-stable:2.12.0 zap-full-scan.py -t https://YOUR_APPLICATION.com.au/ -g gen.conf -r report-full.html -x report-full.xml >> report-full.txt
true - Translate XML to NUnit (PowerShell)
$XSLT = '<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="NumberOfItems" select="count(OWASPZAPReport/site/alerts/alertitem)"/>
<xsl:variable name="generatedDateTime" select="OWASPZAPReport/generated"/>
<xsl:template match="/">
<test-run id="1" name="OWASPReport" fullname="OWASPConvertReport" testcasecount="" result="Failed" total="{$NumberOfItems}" passed="0" failed="{$NumberOfItems}" inconclusive="0" skipped="0" asserts="{$NumberOfItems}" engine-version="3.9.0.0" clr-version="4.0.30319.42000" start-time="{$generatedDateTime}" end-time="{$generatedDateTime}" duration="0">
<command-line>a</command-line>
<test-suite type="Assembly" id="0-1005" name="OWASP" fullname="OWASP" runstate="Runnable" testcasecount="{$NumberOfItems}" result="Failed" site="Child" start-time="{$generatedDateTime}" end-time="{$generatedDateTime}" duration="0.352610" total="{$NumberOfItems}" passed="0" failed="{$NumberOfItems}" warnings="0" inconclusive="0" skipped="0" asserts="{$NumberOfItems}">
<environment framework-version="3.11.0.0" clr-version="4.0.30319.42000" os-version="Microsoft Windows NT 10.0.17763.0" platform="Win32NT" cwd="C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\Common7\IDE" machine-name="Azure Hosted Agent" user="flacroix" user-domain="NORTHAMERICA" culture="en-US" uiculture="en-US" os-architecture="x86" />
<test-suite type="TestSuite" id="0-1004" name="UnitTestDemoTest" fullname="UnitTestDemoTest" runstate="Runnable" testcasecount="2" result="Failed" site="Child" start-time="2019-02-01 17:03:03Z" end-time="2019-02-01 17:03:04Z" duration="0.526290" total="2" passed="1" failed="1" warnings="0" inconclusive="0" skipped="0" asserts="1">
<test-suite type="TestFixture" id="0-1000" name="UnitTest1" fullname="UnitTestDemoTest.UnitTest1" classname="UnitTestDemoTest.UnitTest1" runstate="Runnable" testcasecount="2" result="Failed" site="Child" start-time="2019-02-01 17:03:03Z" end-time="2019-02-01 17:03:04Z" duration="0.495486" total="2" passed="1" failed="1" warnings="0" inconclusive="0" skipped="0" asserts="1">
<xsl:for-each select="OWASPZAPReport/site/alerts/alertitem">
<xsl:variable name="TestResult" select="count(OWASPZAPReport/site/alerts/alertitem/riskcode)"/>
<xsl:if test="riskcode > 1">
<test-case id="0-1001" name="{name}" fullname="{name}" methodname="Stub" classname="UnitTestDemoTest.UnitTest1" runstate="NotRunnable" seed="400881240" result="Failed" label="Invalid" start-time="{$generatedDateTime}" end-time="{$generatedDateTime}" duration="0" asserts="0">
<failure>
<message>
<xsl:value-of select="desc"/>.
<xsl:value-of select="solution"/>
</message>
<stack-trace>
<xsl:for-each select="instances/instance">
<xsl:value-of select="uri"/>, <xsl:value-of select="method"/>, <xsl:value-of select="param"/>,
</xsl:for-each>
</stack-trace>
</failure>
</test-case>
</xsl:if>
<xsl:if test="riskcode < 2">
<test-case id="0-1001" name="{name}" fullname="{name}" methodname="Stub" classname="UnitTestDemoTest.UnitTest1" runstate="NotRunnable" seed="400881240" result="Skipped" label="Invalid" start-time="{$generatedDateTime}" end-time="{$generatedDateTime}" duration="0" asserts="0">
<failure>
<message>
<xsl:value-of select="desc"/>.
<xsl:value-of select="solution"/>
</message>
<stack-trace>
<xsl:for-each select="instances/instance">
<xsl:value-of select="uri"/>, <xsl:value-of select="method"/>, <xsl:value-of select="param"/>,
</xsl:for-each>
</stack-trace>
</failure>
</test-case>
</xsl:if>
</xsl:for-each>
</test-suite>
</test-suite>
</test-suite>
</test-run>
</xsl:template>
</xsl:stylesheet>
' # Create XSLT file
$Folder = Get-Location
$fileName = "OWASP-XML-2-Unit.xslt"
$XsltFilePath = Join-Path -Path $Folder -ChildPath $fileName
New-Item -ItemType File -Name $fileName -Force
$XSLT | Add-Content -Path $XsltFilePath - File Names (PowerShell)
# filenames
$Folder = Get-Location
$fileName = "OWASP-XML-2-Unit.xslt"
$XsltFilePath = Join-Path -Path $Folder -ChildPath $fileName$ScanReport = "report-full.xml"
$TestReport = "report-full-Tests.xml"
$XmlInputPath = Join-Path -Path $Folder -ChildPath $ScanReport
$XmlOutputPath = Join-Path -Path $Folder -ChildPath $TestReportWrite-Host $XsltFilePath
Write-Host $XmlInputPath
Write-Host $XmlOutputPath# transform
$XslTransform = New-Object System.Xml.Xsl.XslCompiledTransform
$XslTransform.Load($XsltFilePath)
$XslTransform.Transform($XmlInputPath, $XmlOutputPath)# Tests Folder
$TestFolderName = "tests"
Write-Host "create $TestFolderName"
if ((Test-Path -Path $TestFolderName) -eq $false) {
$testsFolder = New-Item -Name $TestFolderName -ItemType Directory
}Copy-Item -Path $TestReport -Destination $TestFolderName
- Add tests results to NUnit (PowerShell)
# Add Passed tests from log to Nunit results
function Add-PassedTests {
[CmdletBinding()]
param (
[string]$LogFilename,
[string]$TestFilename,
[string]$OutputFile
)
# Test Case
$testCase = '<test-case id="{id}" name="{line}" fullname="{line}" methodname="Stub" classname="UnitTestDemoTest.UnitTest1" runstate="NotRunnable" seed="400881240" result="Passed" label="Invalid" start-time="" end-time="" duration="0" asserts="0">
<failure>
<message></message>
<stack-trace></stack-trace>
</failure>
</test-case>'
$TestsPassed = @()foreach ($line in Get-Content $LogFilename) {
if (Select-String -InputObject $line -Pattern "^PASS:") {
$count++
$Name = $line.Replace("PASS: ", "")
$Name = $Name.Replace('"', "")
$IdCount = "1-" + $count
$testCase2 = $testCase.Replace("{line}", $Name)
$testCase2 = $testCase2.Replace("{id}", $IdCount)
$TestsPassed += $testCase2
}
}New-Item -Path $OutputFile -ItemType File -Force
$FirstElement = $true
foreach ($line in Get-Content $TestFilename) {if ($line -match "<test-case" -and $FirstElement) {
Add-PassedTests -LogFilename "report-full.txt" -TestFilename "report-full-Tests.xml" -OutputFile "report-full-Tests-2.xml"
foreach ($item in $TestsPassed) {
$item | Add-Content -Path $OutputFile
}
$FirstElement = $false
$line | Add-Content -Path $OutputFile
}
else {
$line | Add-Content -Path $OutputFile
}
}
}
Copy-Item -Path "report-full-Tests-2.xml" -Destination $(System.DefaultWorkingDirectory)\tests - Publish DAST Results (Publish Test Results)
Test result format:
NUnit
tests/*full*Tests-*.xml
Once the pipeline is finalised, you will have a report similar to the following:
Sharing is caring!