├── .gdn └── .gdnsuppress ├── .github └── workflows │ ├── official-build.yml │ ├── on-push-verification.yml │ └── sample-workflow.yml ├── .gitignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── action.yml ├── gulpfile.js ├── lib ├── container-mapping.js ├── main.js ├── msdo-helpers.js ├── msdo-interface.js ├── msdo.js ├── post.js └── pre.js ├── package.json ├── samples ├── Dockerfile ├── IaCMapping │ ├── azure-pipelines.yml │ ├── main.tf │ └── readme.md ├── K8s-cassandra-statefulset.yaml ├── insecure.js ├── insecure.py └── insecure_arm.json ├── sda.sarif ├── src ├── container-mapping.ts ├── main.ts ├── msdo-helpers.ts ├── msdo-interface.ts ├── msdo.ts ├── post.ts └── pre.ts ├── test ├── post.tests.ts ├── pre.tests.ts ├── testCommon.ts └── tsconfig.json └── tsconfig.json /.gdn/.gdnsuppress: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "suppressionSets": { 4 | "default": { 5 | "name": "default", 6 | "createdDate": "2020-06-22 22:21:08Z", 7 | "lastUpdatedDate": "2020-06-22 22:21:08Z" 8 | } 9 | }, 10 | "results": { 11 | "b55100af590384314d06fdb4f25f1b6e86e07aaf69010e8321c240cc3a7b324c": { 12 | "signature": "b55100af590384314d06fdb4f25f1b6e86e07aaf69010e8321c240cc3a7b324c", 13 | "target": "lib/microsoft-security-devops/msdo-installer.js", 14 | "memberOf": [ 15 | "default" 16 | ], 17 | "tool": "ESLint", 18 | "ruleId": "security/detect-non-literal-fs-filename", 19 | "justification": null, 20 | "createdDate": "2020-06-22 22:21:08Z", 21 | "expirationDate": null, 22 | "type": "External" 23 | }, 24 | "5609febb6d784c6ad6aabb0e4624c2e7f8ed212468ef12256e9d1f6c865241a1": { 25 | "signature": "5609febb6d784c6ad6aabb0e4624c2e7f8ed212468ef12256e9d1f6c865241a1", 26 | "target": "lib/microsoft-security-devops/msdo-installer.js", 27 | "memberOf": [ 28 | "default" 29 | ], 30 | "tool": "ESLint", 31 | "ruleId": "security/detect-non-literal-fs-filename", 32 | "justification": null, 33 | "createdDate": "2020-06-22 22:21:08Z", 34 | "expirationDate": null, 35 | "type": "External" 36 | }, 37 | "133408373b8725daf83aa009ca35add815e4deaadd56be6cfe0d8046b2710b91": { 38 | "signature": "133408373b8725daf83aa009ca35add815e4deaadd56be6cfe0d8046b2710b91", 39 | "target": "lib/microsoft-security-devops/msdo-installer.js", 40 | "memberOf": [ 41 | "default" 42 | ], 43 | "tool": "ESLint", 44 | "ruleId": "security/detect-non-literal-fs-filename", 45 | "justification": null, 46 | "createdDate": "2020-06-22 22:21:08Z", 47 | "expirationDate": null, 48 | "type": "External" 49 | }, 50 | "060c07f7a16a8a8c365e4ff1c079e4754ad6c19ff617729953dcea28fb363758": { 51 | "signature": "060c07f7a16a8a8c365e4ff1c079e4754ad6c19ff617729953dcea28fb363758", 52 | "target": "lib/microsoft-security-devops/msdo-installer.js", 53 | "memberOf": [ 54 | "default" 55 | ], 56 | "tool": "ESLint", 57 | "ruleId": "security/detect-non-literal-fs-filename", 58 | "justification": null, 59 | "createdDate": "2020-06-22 22:21:08Z", 60 | "expirationDate": null, 61 | "type": "External" 62 | }, 63 | "4da68ea9226ffbb2659163b8035d7d7ab26492c1009f9696606e32e6d919d874": { 64 | "signature": "4da68ea9226ffbb2659163b8035d7d7ab26492c1009f9696606e32e6d919d874", 65 | "target": "lib/microsoft-security-devops/msdo-installer.js", 66 | "memberOf": [ 67 | "default" 68 | ], 69 | "tool": "ESLint", 70 | "ruleId": "security/detect-non-literal-fs-filename", 71 | "justification": null, 72 | "createdDate": "2020-06-22 22:21:08Z", 73 | "expirationDate": null, 74 | "type": "External" 75 | }, 76 | "00e68c75c3d4c2a4e875be3b3b166ef4b70916316648b6e0e38fd3f81b40ed86": { 77 | "signature": "00e68c75c3d4c2a4e875be3b3b166ef4b70916316648b6e0e38fd3f81b40ed86", 78 | "target": "lib/microsoft-security-devops/msdo-installer.js", 79 | "memberOf": [ 80 | "default" 81 | ], 82 | "tool": "ESLint", 83 | "ruleId": "security/detect-non-literal-fs-filename", 84 | "justification": null, 85 | "createdDate": "2020-06-22 22:21:08Z", 86 | "expirationDate": null, 87 | "type": "External" 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /.github/workflows/official-build.yml: -------------------------------------------------------------------------------- 1 | name: security-devops-action Official Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - release/vNext 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Extract branch name 23 | shell: bash 24 | run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT 25 | id: extract_branch 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v2 29 | with: 30 | node-version: '14' 31 | 32 | - name: Configure npm to use GitHub Packages 33 | run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Compile TypeScript 39 | run: npm run build 40 | 41 | - name: Commit compiled JavaScript 42 | run: | 43 | git config --global user.name 'github-actions[bot]' 44 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 45 | git add lib/. 46 | git commit -m 'Official Build: Compile TypeScript to JavaScript' 47 | git push --force origin HEAD:${{ steps.extract_branch.outputs.branch }} 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/on-push-verification.yml: -------------------------------------------------------------------------------- 1 | # pull request action verification 2 | 3 | name: MSDO On Push Verification 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | 9 | permissions: 10 | id-token: write # This is required for federation to Defender for DevOps 11 | security-events: write # This is required to upload SARIF files 12 | 13 | jobs: 14 | sample: 15 | name: MSDO on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | os: [windows-latest, ubuntu-latest] 21 | 22 | steps: 23 | 24 | # Checkout your code repository to scan 25 | - uses: actions/checkout@v3 26 | 27 | # Run analyzers 28 | - uses: ./ 29 | id: msdo 30 | 31 | # Upload alerts to the Security tab 32 | - name: Upload alerts to Security tab 33 | uses: github/codeql-action/upload-sarif@v2 34 | with: 35 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 36 | 37 | # Upload alerts file as a workflow artifact 38 | - name: Upload alerts file as a workflow artifact 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: alerts 42 | path: ${{ steps.msdo.outputs.sarifFile }} 43 | -------------------------------------------------------------------------------- /.github/workflows/sample-workflow.yml: -------------------------------------------------------------------------------- 1 | name: MSDO Sample Workflow 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | id-token: write # This is required for federation to Defender for DevOps 9 | security-events: write # This is required to upload SARIF files 10 | 11 | jobs: 12 | sample: 13 | name: MSDO on ${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [windows-latest, ubuntu-latest] 19 | 20 | steps: 21 | 22 | # Checkout your code repository to scan 23 | - uses: actions/checkout@v3 24 | 25 | # Run analyzers 26 | - name: Run Microsoft Security DevOps Analysis 27 | uses: microsoft/security-devops-action@v1 28 | id: msdo 29 | 30 | # Upload alerts to the Security tab 31 | - name: Upload alerts to Security tab 32 | uses: github/codeql-action/upload-sarif@v2 33 | with: 34 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 35 | 36 | # Upload alerts file as a workflow artifact 37 | - name: Upload alerts file as a workflow artifact 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: alerts 41 | path: ${{ steps.msdo.outputs.sarifFile }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | test/**/*.js 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | **/Properties/launchSettings.json 57 | 58 | # StyleCop 59 | StyleCopReport.xml 60 | 61 | # Files built by Visual Studio 62 | *_i.c 63 | *_p.c 64 | *_i.h 65 | *.ilk 66 | *.meta 67 | *.obj 68 | *.iobj 69 | *.pch 70 | *.pdb 71 | *.ipdb 72 | *.pgc 73 | *.pgd 74 | *.rsp 75 | *.sbr 76 | *.tlb 77 | *.tli 78 | *.tlh 79 | *.tmp 80 | *.tmp_proj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | *.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush 296 | .cr/ 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | @microsoft:registry=https://npm.pkg.github.com/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microsoft/security-devops-action (Preview) 2 | 3 | Microsoft Security DevOps (MSDO) is a command line application which integrates static analysis tools into the development cycle. MSDO installs, configures and runs the latest versions of static analysis tools (including, but not limited to, SDL/security and compliance tools). MSDO is data-driven with portable configurations that enable deterministic execution across multiple environments. For tools that output results in or MSDO can convert their results to SARIF, MSDO imports into a normalized file database for seamlessly reporting and responding to results across tools, such as forcing build breaks. 4 | 5 | Run locally. Run remotely. 6 | 7 | ![Microsoft Security DevOps](https://github.com/microsoft/security-devops-action/workflows/MSDO%20Sample%20Workflow/badge.svg) 8 | 9 | This action runs the [Microsoft Security DevOps CLI](https://aka.ms/msdo-nuget) for security analysis: 10 | 11 | * Installs the Microsoft Security DevOps CLI 12 | * Installs the latest Microsoft security policy 13 | * Installs the latest Microsoft and 3rd party security tools 14 | * Automatic or user-provided configuration of security tools 15 | * Execution of a full suite of security tools 16 | * Normalized processing of results into the SARIF format 17 | * Build breaks and more 18 | 19 | # Usage 20 | 21 | See [action.yml](action.yml) 22 | 23 | ## Basic 24 | 25 | Run **Microsoft Security DevOps (MSDO)** with the default policy and recommended tools. 26 | 27 | ```yaml 28 | permissions: 29 | security-events: write 30 | 31 | steps: 32 | 33 | - uses: actions/checkout@v3 34 | 35 | - name: Run Microsoft Security DevOps 36 | uses: microsoft/security-devops-action@latest 37 | id: msdo 38 | ``` 39 | 40 | ## Upload Results to the Security tab 41 | 42 | To upload results to the Security tab of your repo, run the `github/codeql-action/upload-sarif` action immediately after running MSDO. MSDO sets the action output variable `sarifFile` to the path of a single SARIF file that can be uploaded to this API. 43 | 44 | ```yaml 45 | - name: Upload results to Security tab 46 | uses: github/codeql-action/upload-sarif@v2 47 | with: 48 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 49 | ``` 50 | 51 | ## Advanced 52 | 53 | To only run specific analyzers, use the `tools` command. This command is a comma-seperated list of tools to run. For example, to run only the `container-mapping` tool, configure this action as follows: 54 | 55 | ```yaml 56 | - uses: microsoft/security-devops-action@latest 57 | id: msdo 58 | with: 59 | tools: container-mapping 60 | ``` 61 | 62 | # Tools 63 | 64 | | Name | Language | License | 65 | | --- | --- | --- | 66 | | [AntiMalware](https://www.microsoft.com/en-us/windows/comprehensive-security) | code, artifacts | - | 67 | | [Bandit](https://github.com/PyCQA/bandit) | python | [Apache License 2.0](https://github.com/PyCQA/bandit/blob/master/LICENSE) | 68 | | [BinSkim](https://github.com/Microsoft/binskim) | binary - Windows, ELF | [MIT License](https://github.com/microsoft/binskim/blob/main/LICENSE) | 69 | | [Checkov](https://github.com/bridgecrewio/checkov) | Infrastructure-as-code (IaC), Terraform, Terraform plan, Cloudformation, AWS SAM, Kubernetes, Helm charts, Kustomize, Dockerfile, Serverless, Bicep, OpenAPI, ARM Templates, or OpenTofu | [Apache License 2.0](https://github.com/bridgecrewio/checkov/blob/main/LICENSE) | 70 | | [ESlint](https://github.com/eslint/eslint) | JavaScript | [MIT License](https://github.com/eslint/eslint/blob/main/LICENSE) | 71 | | [Template Analyzer](https://github.com/Azure/template-analyzer) | Infrastructure-as-code (IaC), ARM templates, Bicep files | [MIT License](https://github.com/Azure/template-analyzer/blob/main/LICENSE.txt) | 72 | | [Terrascan](https://github.com/accurics/terrascan) | Infrastructure-as-code (IaC), Terraform (HCL2), Kubernetes (JSON/YAML), Helm v3, Kustomize, Dockerfiles, Cloudformation | [Apache License 2.0](https://github.com/accurics/terrascan/blob/master/LICENSE) | 73 | | [Trivy](https://github.com/aquasecurity/trivy) | container images, file systems, and git repositories | [Apache License 2.0](https://github.com/aquasecurity/trivy/blob/main/LICENSE) | 74 | | [container-mapping](https://learn.microsoft.com/en-us/azure/defender-for-cloud/container-image-mapping) | container images and registries (only available for DevOps security enabled CSPM plans) | [MIT License](https://github.com/microsoft/security-devops-action/blob/main/LICENSE) | 75 | 76 | # More Information 77 | 78 | Please see the [wiki tab](https://github.com/microsoft/security-devops-action/wiki) for more information and the [Frequently Asked Questions (FAQ)](https://github.com/microsoft/security-devops-action/wiki/FAQ) page. 79 | 80 | # Report Issues 81 | 82 | Please [file a GitHub issue](https://github.com/microsoft/security-devops-action/issues/new) in this repo. To help us investigate the issue, please include a description of the problem, a link to your workflow run (if public), and/or logs from the MSDO action's output. 83 | 84 | # License 85 | 86 | The scripts and documentation in this project are released under the [MIT License](LICENSE) 87 | 88 | # Contributing 89 | 90 | Contributions are welcome! See the [Contributor's Guide](docs/contributors.md). 91 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 10 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 11 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 12 | 13 | ## Microsoft Support Policy 14 | 15 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 16 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'security-devops-action' 2 | description: 'Run security analyzers.' 3 | author: 'Microsoft' 4 | branding: 5 | icon: 'shield' 6 | color: 'black' 7 | inputs: 8 | command: 9 | description: Deprecated, do not use. 10 | config: 11 | description: A file path to a .gdnconfig file. 12 | policy: 13 | description: The name of the well known policy to use. Defaults to GitHub. 14 | default: GitHub 15 | categories: 16 | description: A comma separated list of analyzer categories to run. Values secrets, code, artifacts, IaC, containers. Example IaC,secrets. Defaults to all. 17 | languages: 18 | description: A comma separated list of languages to analyze. Example javascript, typescript. Defaults to all. 19 | tools: 20 | description: A comma separated list of analyzer to run. Example bandit, binskim, container-mapping, eslint, templateanalyzer, terrascan, trivy. 21 | includeTools: 22 | description: Deprecated 23 | existingFilename: 24 | description: A SARIF filename that already exists. If it does, then the normal run will not take place and the file will instead be uploaded to MSDO backend. 25 | outputs: 26 | sarifFile: 27 | description: A file path to a SARIF results file. 28 | runs: 29 | using: 'node20' 30 | main: 'lib/main.js' 31 | pre: 'lib/pre.js' 32 | post: 'lib/post.js' 33 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | const fs = require('fs'); 3 | const gulp = require('gulp'); 4 | const path = require('path'); 5 | const process = require('process'); 6 | const ts = require('gulp-typescript'); 7 | 8 | const tsProject = ts.createProject('tsconfig.json'); 9 | const testTsProject = ts.createProject(path.join(__dirname, 'test', 'tsconfig.json')); 10 | 11 | function clean(cb) { 12 | import('del') 13 | .then((del) => del.deleteSync(['lib'])) 14 | .then(() => cb()); 15 | } 16 | 17 | function sideload(cb) { 18 | if (process.env.SECURITY_DEVOPS_ACTION_BUILD_SIDELOAD === 'true') { 19 | console.log('Sideload mode enabled. Linking @microsoft/security-devops-actions-toolkit'); 20 | 21 | const toolkitSrcDir = path.resolve(path.join(__dirname, '..', 'security-devops-actions-toolkit')); 22 | 23 | if (!fs.existsSync(toolkitSrcDir)) { 24 | throw new Error(`Could not the toolkit repo directory: ${toolkitSrcDir}. Please clone the repo to a parallel directory to this extension repo. Repo homepage: https://github.com/microsoft/security-devops-actions-toolkit`); 25 | } 26 | 27 | const toolkitNodeModulesDir = path.join(__dirname, 'node_modules', '@microsoft', 'security-devops-actions-toolkit'); 28 | 29 | if (!fs.existsSync(toolkitNodeModulesDir)) { 30 | throw new Error(`The node_modules directory for the toolkit does not exist. please run npm install before continuing: ${toolkitNodeModulesDir}`); 31 | } 32 | 33 | if (process.env.SECURITY_DEVOPS_ACTION_BUILD_SIDELOAD_BUILD !== 'false') { 34 | console.log('Building sideload project: npm run build'); 35 | const output = execSync('npm run build', { cwd: toolkitSrcDir, encoding: 'utf8' }); 36 | console.log(output); 37 | } 38 | 39 | console.log(`Clearing the existing toolkit directory: ${toolkitNodeModulesDir}`); 40 | clearDir(toolkitNodeModulesDir); 41 | 42 | const toolkitDistDir = path.join(toolkitSrcDir, 'dist'); 43 | 44 | console.log("Copying sideload build..."); 45 | copyFiles(toolkitDistDir, toolkitNodeModulesDir); 46 | 47 | fs.writeFileSync( 48 | path.join(toolkitNodeModulesDir, '.sideloaded'), 49 | 'This package was built and sideloaded by the security-devops-action build process. Do not commit this file to source control.'); 50 | } 51 | cb(); 52 | } 53 | 54 | function compile(cb) { 55 | tsProject 56 | .src() 57 | .pipe(tsProject()).js 58 | .pipe(gulp.dest('lib')) 59 | .on('end', () => cb()); 60 | } 61 | 62 | function compileTests(cb) { 63 | testTsProject 64 | .src() 65 | .pipe(testTsProject()).js 66 | .pipe(gulp.dest(path.join(__dirname, 'test'))) 67 | .on('end', () => cb()); 68 | } 69 | 70 | function clearDir(dirPath) { 71 | // Get a list of files and subdirectories in the directory 72 | const items = fs.readdirSync(dirPath); 73 | 74 | for (const item of items) { 75 | const itemPath = path.join(dirPath, item); 76 | 77 | if (fs.statSync(itemPath).isFile()) { 78 | fs.unlinkSync(itemPath); 79 | } else { 80 | clearDir(itemPath); 81 | } 82 | } 83 | 84 | // Finally, remove the empty directory 85 | fs.rmdirSync(dirPath); 86 | } 87 | 88 | function copyFiles(srcDir, destDir) { 89 | if (!fs.existsSync(destDir)) { 90 | fs.mkdirSync(destDir, { recursive: true }); 91 | } 92 | 93 | fs.readdirSync(srcDir).forEach((file) => { 94 | const srcFilePath = path.join(srcDir, file); 95 | const destFilePath = path.join(destDir, file); 96 | 97 | if (fs.statSync(srcFilePath).isDirectory()) { 98 | copyFiles(srcFilePath, destFilePath); 99 | } else { 100 | fs.copyFileSync(srcFilePath, destFilePath); 101 | console.log(`Copied ${srcFilePath} to ${destFilePath}`); 102 | } 103 | }); 104 | } 105 | 106 | exports.clean = clean; 107 | exports.compile = compile; 108 | exports.compileTests = compileTests; 109 | exports.build = gulp.series(clean, sideload, compile); 110 | exports.buildTests = gulp.series(exports.build, compileTests); 111 | exports.default = exports.build; -------------------------------------------------------------------------------- /lib/container-mapping.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | exports.ContainerMapping = void 0; 36 | const https = __importStar(require("https")); 37 | const core = __importStar(require("@actions/core")); 38 | const exec = __importStar(require("@actions/exec")); 39 | const os = __importStar(require("os")); 40 | const sendReportRetryCount = 1; 41 | const GetScanContextURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; 42 | const ContainerMappingURL = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; 43 | class ContainerMapping { 44 | constructor() { 45 | this.succeedOnError = true; 46 | } 47 | runPreJob() { 48 | try { 49 | core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); 50 | this._runPreJob(); 51 | } 52 | catch (error) { 53 | core.info("Error in Container Mapping pre-job: " + error); 54 | } 55 | finally { 56 | core.info("::endgroup::"); 57 | } 58 | } 59 | _runPreJob() { 60 | const startTime = new Date().toISOString(); 61 | core.saveState('PreJobStartTime', startTime); 62 | core.info(`PreJobStartTime: ${startTime}`); 63 | } 64 | runMain() { 65 | return __awaiter(this, void 0, void 0, function* () { 66 | }); 67 | } 68 | runPostJob() { 69 | return __awaiter(this, void 0, void 0, function* () { 70 | try { 71 | core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); 72 | yield this._runPostJob(); 73 | } 74 | catch (error) { 75 | core.info("Error in Container Mapping post-job: " + error); 76 | } 77 | finally { 78 | core.info("::endgroup::"); 79 | } 80 | }); 81 | } 82 | _runPostJob() { 83 | return __awaiter(this, void 0, void 0, function* () { 84 | let startTime = core.getState('PreJobStartTime'); 85 | if (startTime.length <= 0) { 86 | startTime = new Date(new Date().getTime() - 10000).toISOString(); 87 | core.debug(`PreJobStartTime not defined, using now-10secs`); 88 | } 89 | core.info(`PreJobStartTime: ${startTime}`); 90 | let reportData = { 91 | dockerVersion: "", 92 | dockerEvents: [], 93 | dockerImages: [] 94 | }; 95 | let bearerToken = yield core.getIDToken() 96 | .then((token) => { return token; }) 97 | .catch((error) => { 98 | throw new Error("Unable to get token: " + error); 99 | }); 100 | if (!bearerToken) { 101 | throw new Error("Empty OIDC token received"); 102 | } 103 | var callerIsOnboarded = yield this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); 104 | if (!callerIsOnboarded) { 105 | core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload."); 106 | return; 107 | } 108 | core.info("Client is onboarded for container mapping."); 109 | let dockerVersionOutput = yield exec.getExecOutput('docker --version'); 110 | if (dockerVersionOutput.exitCode != 0) { 111 | core.info(`Unable to get docker version: ${dockerVersionOutput}`); 112 | core.info(`Skipping container mapping since docker not found/available.`); 113 | return; 114 | } 115 | reportData.dockerVersion = dockerVersionOutput.stdout.trim(); 116 | yield this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) 117 | .catch((error) => { 118 | throw new Error("Unable to get docker events: " + error); 119 | }); 120 | yield this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) 121 | .catch((error) => { 122 | throw new Error("Unable to get docker images: " + error); 123 | }); 124 | core.debug("Finished data collection, starting API calls."); 125 | var reportSent = yield this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); 126 | if (!reportSent) { 127 | throw new Error("Unable to send report to backend service"); 128 | } 129 | ; 130 | core.info("Container mapping data sent successfully."); 131 | }); 132 | } 133 | execCommand(command, listener) { 134 | return __awaiter(this, void 0, void 0, function* () { 135 | return exec.getExecOutput(command) 136 | .then((result) => { 137 | if (result.exitCode != 0) { 138 | return Promise.reject(`Command execution failed: ${result}`); 139 | } 140 | result.stdout.trim().split(os.EOL).forEach(element => { 141 | if (element.length > 0) { 142 | listener.push(element); 143 | } 144 | }); 145 | }); 146 | }); 147 | } 148 | sendReport(data, bearerToken, retryCount = 0) { 149 | return __awaiter(this, void 0, void 0, function* () { 150 | core.debug(`attempting to send report: ${data}`); 151 | return yield this._sendReport(data, bearerToken) 152 | .then(() => { 153 | return true; 154 | }) 155 | .catch((error) => __awaiter(this, void 0, void 0, function* () { 156 | if (retryCount == 0) { 157 | return false; 158 | } 159 | else { 160 | core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); 161 | retryCount--; 162 | return yield this.sendReport(data, bearerToken, retryCount); 163 | } 164 | })); 165 | }); 166 | } 167 | _sendReport(data, bearerToken) { 168 | return __awaiter(this, void 0, void 0, function* () { 169 | return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { 170 | let apiTime = new Date().getMilliseconds(); 171 | let options = { 172 | method: 'POST', 173 | timeout: 2500, 174 | headers: { 175 | 'Content-Type': 'application/json', 176 | 'Authorization': 'Bearer ' + bearerToken, 177 | 'Content-Length': data.length 178 | } 179 | }; 180 | core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); 181 | const req = https.request(ContainerMappingURL, options, (res) => { 182 | let resData = ''; 183 | res.on('data', (chunk) => { 184 | resData += chunk.toString(); 185 | }); 186 | res.on('end', () => { 187 | core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); 188 | core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); 189 | core.debug('Response headers: ' + JSON.stringify(res.headers)); 190 | if (resData.length > 0) { 191 | core.debug('Response: ' + resData); 192 | } 193 | if (res.statusCode < 200 || res.statusCode >= 300) { 194 | return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); 195 | } 196 | resolve(); 197 | }); 198 | }); 199 | req.on('error', (error) => { 200 | reject(new Error(`Error calling url: ${error}`)); 201 | }); 202 | req.write(data); 203 | req.end(); 204 | })); 205 | }); 206 | } 207 | checkCallerIsCustomer(bearerToken, retryCount = 0) { 208 | return __awaiter(this, void 0, void 0, function* () { 209 | return yield this._checkCallerIsCustomer(bearerToken) 210 | .then((statusCode) => __awaiter(this, void 0, void 0, function* () { 211 | if (statusCode == 200) { 212 | return true; 213 | } 214 | else if (statusCode == 403) { 215 | return false; 216 | } 217 | else { 218 | core.debug(`Unexpected status code: ${statusCode}`); 219 | return yield this.retryCall(bearerToken, retryCount); 220 | } 221 | })) 222 | .catch((error) => __awaiter(this, void 0, void 0, function* () { 223 | core.info(`Unexpected error: ${error}.`); 224 | return yield this.retryCall(bearerToken, retryCount); 225 | })); 226 | }); 227 | } 228 | retryCall(bearerToken, retryCount) { 229 | return __awaiter(this, void 0, void 0, function* () { 230 | if (retryCount == 0) { 231 | core.info(`All retries failed.`); 232 | return false; 233 | } 234 | else { 235 | core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); 236 | retryCount--; 237 | return yield this.checkCallerIsCustomer(bearerToken, retryCount); 238 | } 239 | }); 240 | } 241 | _checkCallerIsCustomer(bearerToken) { 242 | return __awaiter(this, void 0, void 0, function* () { 243 | return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { 244 | let options = { 245 | method: 'GET', 246 | timeout: 2500, 247 | headers: { 248 | 'Content-Type': 'application/json', 249 | 'Authorization': 'Bearer ' + bearerToken, 250 | } 251 | }; 252 | core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); 253 | const req = https.request(GetScanContextURL, options, (res) => { 254 | res.on('end', () => { 255 | resolve(res.statusCode); 256 | }); 257 | res.on('data', function (d) { 258 | }); 259 | }); 260 | req.on('error', (error) => { 261 | reject(new Error(`Error calling url: ${error}`)); 262 | }); 263 | req.end(); 264 | })); 265 | }); 266 | } 267 | } 268 | exports.ContainerMapping = ContainerMapping; 269 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | const core = __importStar(require("@actions/core")); 36 | const msdo_1 = require("./msdo"); 37 | const msdo_interface_1 = require("./msdo-interface"); 38 | const common = __importStar(require("@microsoft/security-devops-actions-toolkit/msdo-common")); 39 | const msdo_helpers_1 = require("./msdo-helpers"); 40 | function runMain() { 41 | return __awaiter(this, void 0, void 0, function* () { 42 | if (shouldRunMain()) { 43 | yield (0, msdo_interface_1.getExecutor)(msdo_1.MicrosoftSecurityDevOps).runMain(); 44 | } 45 | else { 46 | console.log("Scanning is not enabled. Skipping..."); 47 | } 48 | }); 49 | } 50 | runMain().catch(error => { 51 | core.setFailed(error); 52 | }); 53 | function shouldRunMain() { 54 | let toolsString = core.getInput('tools'); 55 | if (!common.isNullOrWhiteSpace(toolsString)) { 56 | let tools = toolsString.split(','); 57 | if (tools.length == 1 && tools[0].trim() == msdo_helpers_1.Tools.ContainerMapping) { 58 | return false; 59 | } 60 | } 61 | return true; 62 | } 63 | -------------------------------------------------------------------------------- /lib/msdo-helpers.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.writeToOutStream = exports.getEncodedContent = exports.encode = exports.Constants = exports.Tools = exports.RunnerType = exports.Inputs = void 0; 7 | const os_1 = __importDefault(require("os")); 8 | var Inputs; 9 | (function (Inputs) { 10 | Inputs["Command"] = "command"; 11 | Inputs["Config"] = "config"; 12 | Inputs["Policy"] = "policy"; 13 | Inputs["Categories"] = "categories"; 14 | Inputs["Languages"] = "languages"; 15 | Inputs["Tools"] = "tools"; 16 | Inputs["IncludeTools"] = "includeTools"; 17 | Inputs["ExistingFilename"] = "existingFilename"; 18 | })(Inputs || (exports.Inputs = Inputs = {})); 19 | var RunnerType; 20 | (function (RunnerType) { 21 | RunnerType["Main"] = "main"; 22 | RunnerType["Pre"] = "pre"; 23 | RunnerType["Post"] = "post"; 24 | })(RunnerType || (exports.RunnerType = RunnerType = {})); 25 | var Tools; 26 | (function (Tools) { 27 | Tools["Bandit"] = "bandit"; 28 | Tools["Binskim"] = "binskim"; 29 | Tools["Checkov"] = "checkov"; 30 | Tools["ContainerMapping"] = "container-mapping"; 31 | Tools["ESLint"] = "eslint"; 32 | Tools["TemplateAnalyzer"] = "templateanalyzer"; 33 | Tools["Terrascan"] = "terrascan"; 34 | Tools["Trivy"] = "trivy"; 35 | })(Tools || (exports.Tools = Tools = {})); 36 | var Constants; 37 | (function (Constants) { 38 | Constants["Unknown"] = "unknown"; 39 | Constants["PreJobStartTime"] = "PREJOBSTARTTIME"; 40 | })(Constants || (exports.Constants = Constants = {})); 41 | const encode = (str) => Buffer.from(str, 'binary').toString('base64'); 42 | exports.encode = encode; 43 | function getEncodedContent(dockerVersion, dockerEvents, dockerImages) { 44 | let data = []; 45 | data.push("DockerVersion: " + dockerVersion); 46 | data.push("DockerEvents:"); 47 | data.push(dockerEvents); 48 | data.push("DockerImages:"); 49 | data.push(dockerImages); 50 | return (0, exports.encode)(data.join(os_1.default.EOL)); 51 | } 52 | exports.getEncodedContent = getEncodedContent; 53 | function writeToOutStream(data, outStream = process.stdout) { 54 | outStream.write(data.trim() + os_1.default.EOL); 55 | } 56 | exports.writeToOutStream = writeToOutStream; 57 | -------------------------------------------------------------------------------- /lib/msdo-interface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.getExecutor = void 0; 4 | function getExecutor(runner) { 5 | return new runner(); 6 | } 7 | exports.getExecutor = getExecutor; 8 | -------------------------------------------------------------------------------- /lib/msdo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | exports.MicrosoftSecurityDevOps = void 0; 36 | const core = __importStar(require("@actions/core")); 37 | const msdo_helpers_1 = require("./msdo-helpers"); 38 | const client = __importStar(require("@microsoft/security-devops-actions-toolkit/msdo-client")); 39 | const common = __importStar(require("@microsoft/security-devops-actions-toolkit/msdo-common")); 40 | class MicrosoftSecurityDevOps { 41 | constructor() { 42 | this.succeedOnError = false; 43 | } 44 | runPreJob() { 45 | return __awaiter(this, void 0, void 0, function* () { 46 | }); 47 | } 48 | runPostJob() { 49 | return __awaiter(this, void 0, void 0, function* () { 50 | }); 51 | } 52 | runMain() { 53 | return __awaiter(this, void 0, void 0, function* () { 54 | core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...'); 55 | let args = undefined; 56 | let existingFilename = core.getInput('existingFilename'); 57 | if (!common.isNullOrWhiteSpace(existingFilename)) { 58 | args = ['upload', '--file', existingFilename]; 59 | } 60 | else { 61 | args = ['run']; 62 | let config = core.getInput('config'); 63 | if (!common.isNullOrWhiteSpace(config)) { 64 | args.push('-c'); 65 | args.push(config); 66 | } 67 | let policy = core.getInput('policy'); 68 | if (common.isNullOrWhiteSpace(policy)) { 69 | policy = "GitHub"; 70 | } 71 | args.push('-p'); 72 | args.push(policy); 73 | let categoriesString = core.getInput('categories'); 74 | if (!common.isNullOrWhiteSpace(categoriesString)) { 75 | args.push('--categories'); 76 | let categories = categoriesString.split(','); 77 | for (let i = 0; i < categories.length; i++) { 78 | let category = categories[i]; 79 | if (!common.isNullOrWhiteSpace(category)) { 80 | args.push(category.trim()); 81 | } 82 | } 83 | } 84 | let languagesString = core.getInput('languages'); 85 | if (!common.isNullOrWhiteSpace(languagesString)) { 86 | args.push('--languages'); 87 | let languages = languagesString.split(','); 88 | for (let i = 0; i < languages.length; i++) { 89 | let language = languages[i]; 90 | if (!common.isNullOrWhiteSpace(language)) { 91 | args.push(language.trim()); 92 | } 93 | } 94 | } 95 | let toolsString = core.getInput('tools'); 96 | let includedTools = []; 97 | if (!common.isNullOrWhiteSpace(toolsString)) { 98 | let tools = toolsString.split(','); 99 | for (let i = 0; i < tools.length; i++) { 100 | let tool = tools[i]; 101 | let toolTrimmed = tool.trim(); 102 | if (!common.isNullOrWhiteSpace(tool) 103 | && tool != msdo_helpers_1.Tools.ContainerMapping 104 | && includedTools.indexOf(toolTrimmed) == -1) { 105 | if (includedTools.length == 0) { 106 | args.push('--tool'); 107 | } 108 | args.push(toolTrimmed); 109 | includedTools.push(toolTrimmed); 110 | } 111 | } 112 | } 113 | args.push('--github'); 114 | } 115 | yield client.run(args, 'microsoft/security-devops-action'); 116 | }); 117 | } 118 | } 119 | exports.MicrosoftSecurityDevOps = MicrosoftSecurityDevOps; 120 | -------------------------------------------------------------------------------- /lib/post.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | const core = __importStar(require("@actions/core")); 36 | const container_mapping_1 = require("./container-mapping"); 37 | const msdo_interface_1 = require("./msdo-interface"); 38 | function runPost() { 39 | return __awaiter(this, void 0, void 0, function* () { 40 | yield (0, msdo_interface_1.getExecutor)(container_mapping_1.ContainerMapping).runPostJob(); 41 | }); 42 | } 43 | runPost().catch((error) => { 44 | core.debug(error); 45 | }); 46 | -------------------------------------------------------------------------------- /lib/pre.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 26 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 27 | return new (P || (P = Promise))(function (resolve, reject) { 28 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 29 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 30 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 31 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 32 | }); 33 | }; 34 | Object.defineProperty(exports, "__esModule", { value: true }); 35 | const core = __importStar(require("@actions/core")); 36 | const container_mapping_1 = require("./container-mapping"); 37 | const msdo_interface_1 = require("./msdo-interface"); 38 | function runPre() { 39 | return __awaiter(this, void 0, void 0, function* () { 40 | yield (0, msdo_interface_1.getExecutor)(container_mapping_1.ContainerMapping).runPreJob(); 41 | }); 42 | } 43 | runPre().catch((error) => { 44 | core.debug(error); 45 | }); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microsoft-security-devops-action", 3 | "version": "1.12.0", 4 | "description": "Node dependencies for the microsoft/security-devops-action.", 5 | "scripts": { 6 | "build": "npx gulp", 7 | "buildTests": "npx gulp buildTests", 8 | "test": "npx mocha **/*.tests.js", 9 | "buildAndTest": "npx gulp buildTests & npx mocha **/*.tests.js" 10 | }, 11 | "author": "Microsoft Corporation", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@actions/core": "1.10.0", 15 | "@actions/exec": "1.1.1", 16 | "@microsoft/security-devops-actions-toolkit": "1.11.0" 17 | }, 18 | "devDependencies": { 19 | "@types/mocha": "^2.2.44", 20 | "@types/node": "^20.3.1", 21 | "@types/q": "^1.0.6", 22 | "@types/sinon": "^4.1.2", 23 | "del": "^7.0.0", 24 | "gulp": "^4.0.2", 25 | "gulp-cli": "^2.3.0", 26 | "gulp-shell": "^0.8.0", 27 | "gulp-typescript": "^6.0.0-alpha.1", 28 | "mocha": "^10.2.0", 29 | "sinon": "^4.1.3", 30 | "typescript": "^5.1.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14.0 2 | RUN echo "testuser:x:10999:10999:,,,:/home/testuser:/bin/bash" >> /etc/passwd && echo "testuser::18761:0:99999:7:::" >> /etc/shadow 3 | -------------------------------------------------------------------------------- /samples/IaCMapping/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pool: 4 | vmImage: 'windows-latest' 5 | 6 | steps: 7 | - task: TerraformInstaller@0 8 | inputs: 9 | terraformVersion: '1.0.0' 10 | - checkout: self 11 | 12 | - task: AzureCLI@2 13 | inputs: 14 | azureSubscription: '' 15 | scriptType: 'bash' 16 | scriptLocation: 'inlineScript' 17 | inlineScript: | 18 | az account show 19 | cd ./Modules-Prod 20 | terraform init 21 | terraform plan 22 | terraform apply -auto-approve 23 | 24 | - task: MicrosoftSecurityDevOps@1 25 | displayName: 'Microsoft Security DevOps' 26 | task: MicrosoftSecurityDevOps@1 27 | displayName: 'Microsoft Security DevOps' 28 | # If you want to only run iacfilescanner, uncomment the below lines 29 | # inputs: 30 | # tools: 'iacfilescanner' 31 | -------------------------------------------------------------------------------- /samples/IaCMapping/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | azurerm = { 4 | source = "hashicorp/azurerm" 5 | version = "~> 3.0" # adjust this as per your requirements 6 | } 7 | } 8 | } 9 | 10 | provider "azurerm" { 11 | features {} 12 | } 13 | 14 | resource "azurerm_resource_group" "resourcegroup" { 15 | name = "iacmappingdemo" 16 | location = "Central US" 17 | } 18 | 19 | resource "azurerm_storage_account" "terraformaccount1" { 20 | name = "iacmapping1212" 21 | resource_group_name = azurerm_resource_group.resourcegroup.name 22 | location = "Central US" 23 | account_tier = "Standard" 24 | account_replication_type = "GRS" 25 | 26 | tags = { 27 | "mapping_tag" = "6189b638-15a5-42ec-b934-0d2b8e035ce1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /samples/IaCMapping/readme.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | This folder provides samples for using [Infrastructure as Code mapping](https://learn.microsoft.com/azure/defender-for-cloud/iac-template-mapping) within DevOps security in Microsoft Defender for Cloud. 4 | 5 | This sample deployment should only be performed in non-production subscriptions with **no other Terraform managed resources**. 6 | 7 | Note that we do not choose a backend location to store the state file in this demo. Terraform utilizes a state file to store information about the current state of your managed infrastructure and associated configuration. This file will need to be persisted between different runs of the workflow. The recommended approach is to store this file within an Azure Storage Account or other similar remote backend. Normally, this storage would be provisioned manually or via a separate workflow. The Terraform backend block will need to be updated with your selected storage location (see here for documentation). To learn how to incorporate this, see [here](https://developer.hashicorp.com/terraform/language/settings/backends/azurerm). 8 | 9 | ## Contents 10 | * [main.tf](main.tf) provisions an Azure Storage account through Terraform with a unique mapping_tag. To use this template, ensure you modify the locations, names, and unique GUID. To generate a GUID, use [this website](https://guidgenerator.com/). 11 | * [azure-pipelines.yml](azure-pipelines.yml) is a sample Azure DevOps pipeline that can be used to provision the Terraform code in main.tf as a resource within Azure. It is important to include the MSDO task in your ADO pipeline. 12 | * Requires [Azure Resource Manager service connection](https://learn.microsoft.com/troubleshoot/azure/devops/overview-of-azure-resource-manager-service-connections#create-an-azure-rm-service-connection) with permissions to an Azure subscription. 13 | -------------------------------------------------------------------------------- /samples/K8s-cassandra-statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: "apps/v1" # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1 2 | kind: StatefulSet 3 | metadata: 4 | name: cassandra 5 | labels: 6 | app: cassandra 7 | spec: 8 | serviceName: cassandra 9 | replicas: 3 10 | selector: 11 | matchLabels: 12 | app: cassandra 13 | template: 14 | metadata: 15 | labels: 16 | app: cassandra 17 | spec: 18 | terminationGracePeriodSeconds: 1800 19 | containers: 20 | - name: cassandra 21 | image: gcr.io/google-samples/cassandra:v14 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 7000 25 | name: intra-node 26 | - containerPort: 7001 27 | name: tls-intra-node 28 | - containerPort: 7199 29 | name: jmx 30 | - containerPort: 9042 31 | name: cql 32 | resources: 33 | limits: 34 | cpu: "500m" 35 | memory: 1Gi 36 | requests: 37 | cpu: "500m" 38 | memory: 1Gi 39 | securityContext: 40 | capabilities: 41 | add: 42 | - IPC_LOCK 43 | lifecycle: 44 | preStop: 45 | exec: 46 | command: 47 | - /bin/sh 48 | - -c 49 | - nodetool drain 50 | env: 51 | - name: MAX_HEAP_SIZE 52 | value: 512M 53 | - name: HEAP_NEWSIZE 54 | value: 100M 55 | - name: CASSANDRA_SEEDS 56 | value: "cassandra-0.cassandra.default.svc.cluster.local" 57 | - name: CASSANDRA_CLUSTER_NAME 58 | value: "K8Demo" 59 | - name: CASSANDRA_DC 60 | value: "DC1-K8Demo" 61 | - name: CASSANDRA_RACK 62 | value: "Rack1-K8Demo" 63 | - name: CASSANDRA_SEED_PROVIDER 64 | value: io.k8s.cassandra.KubernetesSeedProvider 65 | - name: POD_IP 66 | valueFrom: 67 | fieldRef: 68 | fieldPath: status.podIP 69 | readinessProbe: 70 | exec: 71 | command: 72 | - /bin/bash 73 | - -c 74 | - /ready-probe.sh 75 | initialDelaySeconds: 15 76 | timeoutSeconds: 5 77 | # These volume mounts are persistent. They are like inline claims, 78 | # but not exactly because the names need to match exactly one of 79 | # the stateful pod volumes. 80 | volumeMounts: 81 | - name: cassandra-data 82 | mountPath: /var/lib/cassandra 83 | # These are converted to volume claims by the controller 84 | # and mounted at the paths mentioned above. 85 | # do not use these in production until ssd GCEPersistentDisk or other ssd pd 86 | volumeClaimTemplates: 87 | - metadata: 88 | name: cassandra-data 89 | annotations: 90 | volume.beta.kubernetes.io/storage-class: fast 91 | spec: 92 | accessModes: [ "ReadWriteOnce" ] 93 | resources: 94 | requests: 95 | storage: 1Gi 96 | --- 97 | kind: StorageClass 98 | apiVersion: storage.k8s.io/v1 99 | metadata: 100 | name: fast 101 | provisioner: k8s.io/minikube-hostpath 102 | parameters: 103 | type: pd-ssd 104 | -------------------------------------------------------------------------------- /samples/insecure.js: -------------------------------------------------------------------------------- 1 | let injection = "Hello, security vulnerabilities!"; 2 | eval(`console.log(\"${injection}\");`); -------------------------------------------------------------------------------- /samples/insecure.py: -------------------------------------------------------------------------------- 1 | # Commented out sample to pass scanning 2 | # 3 | #import hashlib 4 | # print("I am very insecure. Bandit thinks so too.") 5 | # #B110 6 | # xs=[1,2,3,4,5,6,7,8] 7 | # try: 8 | # print(xs[7]) 9 | # print(xs[8]) 10 | # except: pass 11 | 12 | # ys=[1, 2, None, None] 13 | # for y in ys: 14 | # try: 15 | # print(str(y+3)) #TypeErrors ahead 16 | # except: continue #not how to handle them 17 | 18 | # #some imports 19 | # import telnetlib 20 | # import ftplib 21 | 22 | # #B303 and B324 23 | # s = b"I am a string" 24 | # print("MD5: " +hashlib.md5(s).hexdigest()) 25 | # print("SHA1: " +hashlib.sha1(s).hexdigest()) 26 | # print("SHA256: " +hashlib.sha256(s).hexdigest()) 27 | -------------------------------------------------------------------------------- /samples/insecure_arm.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "location": { 6 | "type": "string", 7 | "defaultValue": "[resourceGroup().location]", 8 | "metadata": { 9 | "description": "Location for all resources." 10 | } 11 | } 12 | }, 13 | "resources": [ 14 | { 15 | "apiVersion": "2019-08-01", 16 | "type": "Microsoft.Web/serverfarms", 17 | "name": "serverFarm", 18 | "location": "[parameters('location')]" 19 | }, 20 | { 21 | "apiVersion": "2019-08-01", 22 | "type": "Microsoft.Web/sites", 23 | "kind": "api", 24 | "name": "ApiAppNoHttps", 25 | "location": "[parameters('location')]", 26 | "dependsOn": [ 27 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 28 | ], 29 | "properties": { 30 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 31 | } 32 | }, 33 | { 34 | "apiVersion": "2019-08-01", 35 | "type": "Microsoft.Web/sites", 36 | "kind": "api", 37 | "name": "ApiApp_HttpsFalse", 38 | "location": "[parameters('location')]", 39 | "dependsOn": [ 40 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 41 | ], 42 | "properties": { 43 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 44 | "httpsOnly": false 45 | } 46 | }, 47 | { 48 | "apiVersion": "2019-08-01", 49 | "type": "Microsoft.Web/sites", 50 | "kind": "api", 51 | "name": "ApiApp_HttpsTrue", 52 | "location": "[parameters('location')]", 53 | "dependsOn": [ 54 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 55 | ], 56 | "properties": { 57 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 58 | "httpsOnly": true 59 | } 60 | }, 61 | { 62 | "apiVersion": "2019-08-01", 63 | "type": "Microsoft.Web/sites", 64 | "kind": "functionapp", 65 | "name": "FunctionAppNoHttps", 66 | "location": "[parameters('location')]", 67 | "dependsOn": [ 68 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 69 | ], 70 | "properties": { 71 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 72 | } 73 | }, 74 | { 75 | "apiVersion": "2019-08-01", 76 | "type": "Microsoft.Web/sites", 77 | "kind": "functionapp,linux", 78 | "name": "FunctionApp_HttpsFalse", 79 | "location": "[parameters('location')]", 80 | "dependsOn": [ 81 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 82 | ], 83 | "properties": { 84 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 85 | "httpsOnly": false 86 | } 87 | }, 88 | { 89 | "apiVersion": "2019-08-01", 90 | "type": "Microsoft.Web/sites", 91 | "kind": "functionapp", 92 | "name": "FunctionApp_HttpsTrue", 93 | "location": "[parameters('location')]", 94 | "dependsOn": [ 95 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 96 | ], 97 | "properties": { 98 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 99 | "httpsOnly": true 100 | } 101 | }, 102 | { 103 | "apiVersion": "2019-08-01", 104 | "type": "Microsoft.Web/sites", 105 | "kind": "app,linux", 106 | "name": "WebAppNoHttps", 107 | "location": "[parameters('location')]", 108 | "dependsOn": [ 109 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 110 | ], 111 | "properties": { 112 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 113 | } 114 | }, 115 | { 116 | "apiVersion": "2019-08-01", 117 | "type": "Microsoft.Web/sites", 118 | "name": "WebApp_HttpsFalse", 119 | "location": "[parameters('location')]", 120 | "dependsOn": [ 121 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 122 | ], 123 | "properties": { 124 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 125 | "httpsOnly": false 126 | } 127 | }, 128 | { 129 | "apiVersion": "2019-08-01", 130 | "type": "Microsoft.Web/sites", 131 | "kind": "app", 132 | "name": "WebApp_HttpsTrue", 133 | "location": "[parameters('location')]", 134 | "dependsOn": [ 135 | "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]" 136 | ], 137 | "properties": { 138 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', 'serverFarm')]", 139 | "httpsOnly": true 140 | } 141 | }, 142 | { 143 | "apiVersion": "2019-08-01", 144 | "type": "Microsoft.Web/sites", 145 | "kind": "api", 146 | "name": "ApiApp_RestrictedCORSAccess_EmbeddedSitesConfig", 147 | "location": "[parameters('location')]", 148 | "properties": { 149 | "httpsOnly": true, 150 | "siteConfig": { 151 | "cors": { 152 | "allowedOrigins": [ 153 | "someIP" 154 | ] 155 | } 156 | } 157 | } 158 | }, 159 | { 160 | "apiVersion": "2019-08-01", 161 | "type": "Microsoft.Web/sites", 162 | "kind": "api", 163 | "name": "ApiApp_NoSitesConfig", 164 | "location": "[parameters('location')]", 165 | "properties": { 166 | "httpsOnly": true 167 | } 168 | }, 169 | { 170 | "apiVersion": "2019-08-01", 171 | "type": "Microsoft.Web/sites/config", 172 | "name": "SitesConfig/RestrictedCORSAccess_web", 173 | "location": "[parameters('location')]", 174 | "dependsOn": [ 175 | "ApiApp_NoSitesConfig", 176 | "WebApp_NoSitesConfig", 177 | "FunctionApp_NoSitesConfig" 178 | ], 179 | "properties": { 180 | "cors": { 181 | "allowedOrigins": [ 182 | "someIP" 183 | ] 184 | } 185 | } 186 | }, 187 | { 188 | "apiVersion": "2019-08-01", 189 | "type": "Microsoft.Web/sites", 190 | "kind": "api", 191 | "name": "ApiApp_UnrestrictedCORSAccess_EmbeddedSitesConfig", 192 | "location": "[parameters('location')]", 193 | "properties": { 194 | "httpsOnly": true, 195 | "siteConfig": { 196 | "cors": { 197 | "allowedOrigins": [ 198 | "someIP", 199 | "*" 200 | ] 201 | } 202 | } 203 | } 204 | }, 205 | { 206 | "apiVersion": "2019-08-01", 207 | "type": "Microsoft.Web/sites/config", 208 | "name": "SitesConfig/UnrestrictedCORSAccess_web", 209 | "location": "[parameters('location')]", 210 | "dependsOn": [ 211 | "ApiApp_NoSitesConfig", 212 | "WebApp_NoSitesConfig", 213 | "FunctionApp_NoSitesConfig" 214 | ], 215 | "properties": { 216 | "cors": { 217 | "allowedOrigins": [ 218 | "*" 219 | ] 220 | } 221 | } 222 | }, 223 | { 224 | "apiVersion": "2019-08-01", 225 | "type": "Microsoft.Web/sites", 226 | "kind": "app", 227 | "name": "WebApp_RestrictedCORSAccess_EmbeddedSitesConfig", 228 | "location": "[parameters('location')]", 229 | "properties": { 230 | "httpsOnly": true, 231 | "siteConfig": { 232 | "cors": { 233 | "allowedOrigins": [ 234 | "someIP" 235 | ] 236 | } 237 | } 238 | } 239 | }, 240 | { 241 | "apiVersion": "2019-08-01", 242 | "type": "Microsoft.Web/sites", 243 | "name": "WebApp_NoKind_RestrictedCORSAccess_EmbeddedSitesConfig", 244 | "location": "[parameters('location')]", 245 | "properties": { 246 | "httpsOnly": true, 247 | "siteConfig": { 248 | "cors": { 249 | "allowedOrigins": [ 250 | "someIP" 251 | ] 252 | } 253 | } 254 | } 255 | }, 256 | { 257 | "apiVersion": "2019-08-01", 258 | "type": "Microsoft.Web/sites", 259 | "kind": "app", 260 | "name": "WebApp_UnrestrictedCORSAccess_EmbeddedSitesConfig", 261 | "location": "[parameters('location')]", 262 | "properties": { 263 | "httpsOnly": true, 264 | "siteConfig": { 265 | "cors": { 266 | "allowedOrigins": [ 267 | "someIP", 268 | "*" 269 | ] 270 | } 271 | } 272 | } 273 | }, 274 | { 275 | "apiVersion": "2019-08-01", 276 | "type": "Microsoft.Web/sites", 277 | "kind": "app", 278 | "name": "WebApp_NoSitesConfig", 279 | "location": "[parameters('location')]", 280 | "properties": { 281 | "httpsOnly": true 282 | } 283 | }, 284 | { 285 | "apiVersion": "2019-08-01", 286 | "type": "Microsoft.Web/sites", 287 | "kind": "functionapp", 288 | "name": "FunctionApp_RestrictedCORSAccess_EmbeddedSitesConfig", 289 | "location": "[parameters('location')]", 290 | "properties": { 291 | "httpsOnly": true, 292 | "siteConfig": { 293 | "cors": { 294 | "allowedOrigins": [ 295 | "someIP" 296 | ] 297 | } 298 | } 299 | } 300 | }, 301 | { 302 | "apiVersion": "2019-08-01", 303 | "type": "Microsoft.Web/sites", 304 | "kind": "functionapp", 305 | "name": "FunctionApp_UnrestrictedCORSAccess_EmbeddedSitesConfig", 306 | "location": "[parameters('location')]", 307 | "properties": { 308 | "httpsOnly": true, 309 | "siteConfig": { 310 | "cors": { 311 | "allowedOrigins": [ 312 | "someIP", 313 | "*" 314 | ] 315 | } 316 | } 317 | } 318 | }, 319 | { 320 | "apiVersion": "2019-08-01", 321 | "type": "Microsoft.Web/sites", 322 | "kind": "functionapp", 323 | "name": "FunctionApp_NoSitesConfig", 324 | "location": "[parameters('location')]", 325 | "properties": { 326 | "httpsOnly": true 327 | } 328 | } 329 | ] 330 | } 331 | -------------------------------------------------------------------------------- /src/container-mapping.ts: -------------------------------------------------------------------------------- 1 | import { IMicrosoftSecurityDevOps } from "./msdo-interface"; 2 | import * as https from "https"; 3 | import * as core from '@actions/core'; 4 | import * as exec from '@actions/exec'; 5 | import * as os from 'os'; 6 | 7 | const sendReportRetryCount: number = 1; 8 | const GetScanContextURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/auth-push/GetScanContext?context=authOnly"; 9 | const ContainerMappingURL: string = "https://dfdinfra-afdendpoint-prod-d5fqbucbg7fue0cf.z01.azurefd.net/github/v1/container-mappings"; 10 | 11 | /** 12 | * Represents the tasks for container mapping that are used to fetch Docker images pushed in a job run. 13 | */ 14 | export class ContainerMapping implements IMicrosoftSecurityDevOps { 15 | readonly succeedOnError: boolean; 16 | 17 | constructor() { 18 | this.succeedOnError = true; 19 | } 20 | 21 | /** 22 | * Container mapping pre-job commands wrapped in exception handling. 23 | */ 24 | public runPreJob() { 25 | try { 26 | core.info("::group::Microsoft Defender for DevOps container mapping pre-job - https://go.microsoft.com/fwlink/?linkid=2231419"); 27 | this._runPreJob(); 28 | } 29 | catch (error) { 30 | // Log the error 31 | core.info("Error in Container Mapping pre-job: " + error); 32 | } 33 | finally { 34 | // End the collapsible section 35 | core.info("::endgroup::"); 36 | } 37 | } 38 | 39 | 40 | /* 41 | * Set the start time of the job run. 42 | */ 43 | private _runPreJob() { 44 | const startTime = new Date().toISOString(); 45 | core.saveState('PreJobStartTime', startTime); 46 | core.info(`PreJobStartTime: ${startTime}`); 47 | } 48 | 49 | /** 50 | * Placeholder / interface satisfier for main operations 51 | */ 52 | public async runMain() { 53 | // No commands 54 | } 55 | 56 | /** 57 | * Container mapping post-job commands wrapped in exception handling. 58 | */ 59 | public async runPostJob() { 60 | try { 61 | core.info("::group::Microsoft Defender for DevOps container mapping post-job - https://go.microsoft.com/fwlink/?linkid=2231419"); 62 | await this._runPostJob(); 63 | } catch (error) { 64 | // Log the error 65 | core.info("Error in Container Mapping post-job: " + error); 66 | } finally { 67 | // End the collapsible section 68 | core.info("::endgroup::"); 69 | } 70 | } 71 | 72 | /* 73 | * Using the start time, fetch the docker events and docker images in this job run and log the encoded output 74 | * Send the report to Defender for DevOps 75 | */ 76 | private async _runPostJob() { 77 | let startTime = core.getState('PreJobStartTime'); 78 | if (startTime.length <= 0) { 79 | startTime = new Date(new Date().getTime() - 10000).toISOString(); 80 | core.debug(`PreJobStartTime not defined, using now-10secs`); 81 | } 82 | core.info(`PreJobStartTime: ${startTime}`); 83 | 84 | let reportData = { 85 | dockerVersion: "", 86 | dockerEvents: [], 87 | dockerImages: [] 88 | }; 89 | 90 | let bearerToken: string | void = await core.getIDToken() 91 | .then((token) => { return token; }) 92 | .catch((error) => { 93 | throw new Error("Unable to get token: " + error); 94 | }); 95 | 96 | if (!bearerToken) { 97 | throw new Error("Empty OIDC token received"); 98 | } 99 | 100 | // Don't run the container mapping workload if this caller isn't an active customer. 101 | var callerIsOnboarded: boolean = await this.checkCallerIsCustomer(bearerToken, sendReportRetryCount); 102 | if (!callerIsOnboarded) { 103 | core.info("Client is not onboarded to Defender for DevOps. Skipping container mapping workload.") 104 | return; 105 | } 106 | core.info("Client is onboarded for container mapping."); 107 | 108 | // Initialize the commands 109 | let dockerVersionOutput = await exec.getExecOutput('docker --version'); 110 | if (dockerVersionOutput.exitCode != 0) { 111 | core.info(`Unable to get docker version: ${dockerVersionOutput}`); 112 | core.info(`Skipping container mapping since docker not found/available.`); 113 | return; 114 | } 115 | reportData.dockerVersion = dockerVersionOutput.stdout.trim(); 116 | 117 | await this.execCommand(`docker events --since ${startTime} --until ${new Date().toISOString()} --filter event=push --filter type=image --format ID={{.ID}}`, reportData.dockerEvents) 118 | .catch((error) => { 119 | throw new Error("Unable to get docker events: " + error); 120 | }); 121 | 122 | await this.execCommand(`docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}`, reportData.dockerImages) 123 | .catch((error) => { 124 | throw new Error("Unable to get docker images: " + error); 125 | }); 126 | 127 | core.debug("Finished data collection, starting API calls."); 128 | 129 | var reportSent: boolean = await this.sendReport(JSON.stringify(reportData), bearerToken, sendReportRetryCount); 130 | if (!reportSent) { 131 | throw new Error("Unable to send report to backend service"); 132 | }; 133 | core.info("Container mapping data sent successfully."); 134 | } 135 | 136 | /** 137 | * Execute command and setup the listener to capture the output 138 | * @param command Command to execute 139 | * @param listener Listener to capture the output 140 | * @returns a Promise 141 | */ 142 | private async execCommand(command: string, listener: string[]): Promise { 143 | return exec.getExecOutput(command) 144 | .then((result) => { 145 | if(result.exitCode != 0) { 146 | return Promise.reject(`Command execution failed: ${result}`); 147 | } 148 | result.stdout.trim().split(os.EOL).forEach(element => { 149 | if(element.length > 0) { 150 | listener.push(element); 151 | } 152 | }); 153 | }); 154 | } 155 | 156 | /** 157 | * Sends a report to Defender for DevOps and retries on the specified count 158 | * @param data the data to send 159 | * @param retryCount the number of time to retry 160 | * @param bearerToken the GitHub-generated OIDC token 161 | * @returns a boolean Promise to indicate if the report was sent successfully or not 162 | */ 163 | private async sendReport(data: string, bearerToken: string, retryCount: number = 0): Promise { 164 | core.debug(`attempting to send report: ${data}`); 165 | return await this._sendReport(data, bearerToken) 166 | .then(() => { 167 | return true; 168 | }) 169 | .catch(async (error) => { 170 | if (retryCount == 0) { 171 | return false; 172 | } else { 173 | core.info(`Retrying API call due to error: ${error}.\nRetry count: ${retryCount}`); 174 | retryCount--; 175 | return await this.sendReport(data, bearerToken, retryCount); 176 | } 177 | }); 178 | } 179 | 180 | /** 181 | * Sends a report to Defender for DevOps 182 | * @param data the data to send 183 | * @returns a Promise 184 | */ 185 | private async _sendReport(data: string, bearerToken: string): Promise { 186 | return new Promise(async (resolve, reject) => { 187 | let apiTime = new Date().getMilliseconds(); 188 | let options = { 189 | method: 'POST', 190 | timeout: 2500, 191 | headers: { 192 | 'Content-Type': 'application/json', 193 | 'Authorization': 'Bearer ' + bearerToken, 194 | 'Content-Length': data.length 195 | } 196 | }; 197 | core.debug(`${options['method'].toUpperCase()} ${ContainerMappingURL}`); 198 | 199 | const req = https.request(ContainerMappingURL, options, (res) => { 200 | let resData = ''; 201 | res.on('data', (chunk) => { 202 | resData += chunk.toString(); 203 | }); 204 | 205 | res.on('end', () => { 206 | core.debug('API calls finished. Time taken: ' + (new Date().getMilliseconds() - apiTime) + "ms"); 207 | core.debug(`Status code: ${res.statusCode} ${res.statusMessage}`); 208 | core.debug('Response headers: ' + JSON.stringify(res.headers)); 209 | if (resData.length > 0) { 210 | core.debug('Response: ' + resData); 211 | } 212 | if (res.statusCode < 200 || res.statusCode >= 300) { 213 | return reject(`Received Failed Status code when calling url: ${res.statusCode} ${resData}`); 214 | } 215 | resolve(); 216 | }); 217 | }); 218 | 219 | req.on('error', (error) => { 220 | reject(new Error(`Error calling url: ${error}`)); 221 | }); 222 | 223 | req.write(data); 224 | req.end(); 225 | }); 226 | } 227 | 228 | /** 229 | * Queries Defender for DevOps to determine if the caller is onboarded for container mapping. 230 | * @param retryCount the number of time to retry 231 | * @param bearerToken the GitHub-generated OIDC token 232 | * @returns a boolean Promise to indicate if the report was sent successfully or not 233 | */ 234 | private async checkCallerIsCustomer(bearerToken: string, retryCount: number = 0): Promise { 235 | return await this._checkCallerIsCustomer(bearerToken) 236 | .then(async (statusCode) => { 237 | if (statusCode == 200) { // Status 'OK' means the caller is an onboarded customer. 238 | return true; 239 | } else if (statusCode == 403) { // Status 'Forbidden' means caller is not a customer. 240 | return false; 241 | } else { 242 | core.debug(`Unexpected status code: ${statusCode}`); 243 | return await this.retryCall(bearerToken, retryCount); 244 | } 245 | }) 246 | .catch(async (error) => { 247 | core.info(`Unexpected error: ${error}.`); 248 | return await this.retryCall(bearerToken, retryCount); 249 | }); 250 | } 251 | 252 | private async retryCall(bearerToken: string, retryCount: number): Promise { 253 | if (retryCount == 0) { 254 | core.info(`All retries failed.`); 255 | return false; 256 | } else { 257 | core.info(`Retrying checkCallerIsCustomer.\nRetry count: ${retryCount}`); 258 | retryCount--; 259 | return await this.checkCallerIsCustomer(bearerToken, retryCount); 260 | } 261 | } 262 | 263 | private async _checkCallerIsCustomer(bearerToken: string): Promise { 264 | return new Promise(async (resolve, reject) => { 265 | let options = { 266 | method: 'GET', 267 | timeout: 2500, 268 | headers: { 269 | 'Content-Type': 'application/json', 270 | 'Authorization': 'Bearer ' + bearerToken, 271 | } 272 | }; 273 | core.debug(`${options['method'].toUpperCase()} ${GetScanContextURL}`); 274 | 275 | const req = https.request(GetScanContextURL, options, (res) => { 276 | 277 | res.on('end', () => { 278 | resolve(res.statusCode); 279 | }); 280 | res.on('data', function(d) { 281 | }); 282 | }); 283 | 284 | req.on('error', (error) => { 285 | reject(new Error(`Error calling url: ${error}`)); 286 | }); 287 | 288 | req.end(); 289 | }); 290 | } 291 | 292 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { MicrosoftSecurityDevOps } from './msdo'; 3 | import { getExecutor } from './msdo-interface'; 4 | import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; 5 | import { Tools } from './msdo-helpers'; 6 | 7 | async function runMain() { 8 | if (shouldRunMain()) 9 | { 10 | await getExecutor(MicrosoftSecurityDevOps).runMain(); 11 | } 12 | else { 13 | console.log("Scanning is not enabled. Skipping..."); 14 | } 15 | } 16 | 17 | runMain().catch(error => { 18 | core.setFailed(error); 19 | }); 20 | 21 | /** 22 | * Returns false if the 'tools' input is specified and the only tool on the list is 'container-mapping'. 23 | * This is because the MicrosoftSecurityDevOps executer does not have a workload for the container-mapping tool. 24 | */ 25 | function shouldRunMain() { 26 | let toolsString: string = core.getInput('tools'); 27 | if (!common.isNullOrWhiteSpace(toolsString)) { 28 | let tools = toolsString.split(','); 29 | if (tools.length == 1 && tools[0].trim() == Tools.ContainerMapping) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } -------------------------------------------------------------------------------- /src/msdo-helpers.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import { Writable } from "stream"; 3 | 4 | /** 5 | * Enum for the possible inputs for the task (specified in action.yml) 6 | */ 7 | export enum Inputs { 8 | Command = 'command', 9 | Config = 'config', 10 | Policy = 'policy', 11 | Categories = 'categories', 12 | Languages = 'languages', 13 | Tools = 'tools', 14 | IncludeTools = 'includeTools', 15 | ExistingFilename = 'existingFilename' 16 | } 17 | 18 | /** 19 | * Enum for the runner of the action. 20 | */ 21 | export enum RunnerType { 22 | Main = 'main', 23 | Pre = 'pre', 24 | Post = 'post' 25 | } 26 | 27 | /* 28 | * Enum for the possible values for the Inputs.Tools (specified in action.yml) 29 | */ 30 | export enum Tools { 31 | Bandit = 'bandit', 32 | Binskim = 'binskim', 33 | Checkov = 'checkov', 34 | ContainerMapping = 'container-mapping', 35 | ESLint = 'eslint', 36 | TemplateAnalyzer = 'templateanalyzer', 37 | Terrascan = 'terrascan', 38 | Trivy = 'trivy' 39 | } 40 | 41 | /** 42 | * Enum for defining constants used in the task. 43 | */ 44 | export enum Constants { 45 | Unknown = "unknown", 46 | PreJobStartTime = "PREJOBSTARTTIME" 47 | } 48 | 49 | /** 50 | * Encodes a string to base64. 51 | * 52 | * @param str - The string to encode. 53 | * @returns The base64 encoded string. 54 | */ 55 | export const encode = (str: string):string => Buffer.from(str, 'binary').toString('base64'); 56 | 57 | /** 58 | * Returns the encoded content of the Docker version, Docker events, and Docker images in the pre-defined format - 59 | * DockerVersion 60 | * Version: TaskVersion 61 | * Events: 62 | * DockerEvents 63 | * Images: 64 | * DockerImages 65 | * 66 | * @param dockerVersion - The version of Docker. 67 | * @param dockerEvents - The Docker events. 68 | * @param dockerImages - The Docker images. 69 | * @param taskVersion - Optional version of the task. Defaults to the version in the action.yml file. 70 | * @param sectionDelim - Optional delimiter to separate sections in the encoded content. Defaults to ":::". 71 | * @returns The encoded content of the Docker version, Docker events, and Docker images. 72 | */ 73 | export function getEncodedContent( 74 | dockerVersion: string, 75 | dockerEvents: string, 76 | dockerImages: string 77 | ): string { 78 | let data : string[] = []; 79 | data.push("DockerVersion: " + dockerVersion); 80 | data.push("DockerEvents:"); 81 | data.push(dockerEvents); 82 | data.push("DockerImages:"); 83 | data.push(dockerImages); 84 | return encode(data.join(os.EOL)); 85 | } 86 | 87 | /** 88 | * Writes the specified data to the specified output stream, followed by the platform-specific end-of-line character. 89 | * If no output stream is specified, the data is written to the standard output stream. 90 | * 91 | * @param data - The data to write to the output stream. 92 | * @param outStream - Optional. The output stream to write the data to. Defaults to the standard output stream. 93 | */ 94 | export function writeToOutStream(data: string, outStream: Writable = process.stdout): void { 95 | outStream.write(data.trim() + os.EOL); 96 | } -------------------------------------------------------------------------------- /src/msdo-interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Interface for the MicrosoftSecurityDevOps task 3 | */ 4 | export interface IMicrosoftSecurityDevOps { 5 | readonly succeedOnError: boolean; 6 | /* param source - The source of the task: main, pre, or post. */ 7 | runPreJob(): any; 8 | runMain(): any; 9 | runPostJob(): any; 10 | } 11 | 12 | /** 13 | * Factory interface for creating instances of the `IMicrosoftSecurityDevOps` interface. 14 | * This factory enforces the inputs that can be used for creation of the `IMicrosoftSecurityDevOps` instances. 15 | */ 16 | export interface IMicrosoftSecurityDevOpsFactory { 17 | new (): IMicrosoftSecurityDevOps; 18 | } 19 | 20 | /** 21 | * Returns an instance of IMicrosoftSecurityDevOps based on the input runner and command type. 22 | * (This is used to enforce strong typing for the inputs for the runner). 23 | * @param runner - The runner to use to create the instance of IMicrosoftSecurityDevOps. 24 | * @param commandType - The input command type. 25 | * @returns An instance of IMicrosoftSecurityDevOps. 26 | */ 27 | export function getExecutor(runner: IMicrosoftSecurityDevOpsFactory): IMicrosoftSecurityDevOps { 28 | return new runner(); 29 | } -------------------------------------------------------------------------------- /src/msdo.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { IMicrosoftSecurityDevOps } from './msdo-interface'; 3 | import { Tools } from './msdo-helpers'; 4 | import * as client from '@microsoft/security-devops-actions-toolkit/msdo-client'; 5 | import * as common from '@microsoft/security-devops-actions-toolkit/msdo-common'; 6 | 7 | /* 8 | * Microsoft Security DevOps analyzers runner. 9 | */ 10 | export class MicrosoftSecurityDevOps implements IMicrosoftSecurityDevOps { 11 | readonly succeedOnError: boolean; 12 | 13 | constructor() { 14 | this.succeedOnError = false; 15 | } 16 | 17 | public async runPreJob() { 18 | // No pre-job commands yet 19 | } 20 | 21 | public async runPostJob() { 22 | // No post-job commands yet 23 | } 24 | 25 | public async runMain() { 26 | core.debug('MicrosoftSecurityDevOps.runMain - Running MSDO...'); 27 | 28 | let args: string[] = undefined; 29 | 30 | // Check job type - might be existing file 31 | let existingFilename = core.getInput('existingFilename'); 32 | if (!common.isNullOrWhiteSpace(existingFilename)) { 33 | args = ['upload', '--file', existingFilename]; 34 | } 35 | 36 | // Nope, run the tool as intended 37 | else { 38 | args = ['run']; 39 | 40 | let config: string = core.getInput('config'); 41 | if (!common.isNullOrWhiteSpace(config)) { 42 | args.push('-c'); 43 | args.push(config); 44 | } 45 | 46 | let policy: string = core.getInput('policy'); 47 | if (common.isNullOrWhiteSpace(policy)) { 48 | policy = "GitHub"; 49 | } 50 | 51 | args.push('-p'); 52 | args.push(policy); 53 | 54 | let categoriesString: string = core.getInput('categories'); 55 | if (!common.isNullOrWhiteSpace(categoriesString)) { 56 | args.push('--categories'); 57 | let categories = categoriesString.split(','); 58 | for (let i = 0; i < categories.length; i++) { 59 | let category = categories[i]; 60 | if (!common.isNullOrWhiteSpace(category)) { 61 | args.push(category.trim()); 62 | } 63 | } 64 | } 65 | 66 | let languagesString: string = core.getInput('languages'); 67 | if (!common.isNullOrWhiteSpace(languagesString)) { 68 | args.push('--languages'); 69 | let languages = languagesString.split(','); 70 | for (let i = 0; i < languages.length; i++) { 71 | let language = languages[i]; 72 | if (!common.isNullOrWhiteSpace(language)) { 73 | args.push(language.trim()); 74 | } 75 | } 76 | } 77 | 78 | let toolsString: string = core.getInput('tools'); 79 | let includedTools = []; 80 | if (!common.isNullOrWhiteSpace(toolsString)) { 81 | let tools = toolsString.split(','); 82 | for (let i = 0; i < tools.length; i++) { 83 | let tool = tools[i]; 84 | let toolTrimmed = tool.trim(); 85 | if (!common.isNullOrWhiteSpace(tool) 86 | && tool != Tools.ContainerMapping // This tool is not handled by this executor 87 | && includedTools.indexOf(toolTrimmed) == -1) { 88 | if (includedTools.length == 0) { 89 | args.push('--tool'); 90 | } 91 | args.push(toolTrimmed); 92 | includedTools.push(toolTrimmed); 93 | } 94 | } 95 | } 96 | 97 | args.push('--github'); 98 | } 99 | 100 | await client.run(args, 'microsoft/security-devops-action'); 101 | } 102 | } -------------------------------------------------------------------------------- /src/post.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { ContainerMapping } from './container-mapping'; 3 | import { getExecutor } from './msdo-interface'; 4 | 5 | async function runPost() { 6 | await getExecutor(ContainerMapping).runPostJob(); 7 | } 8 | 9 | runPost().catch((error) => { 10 | core.debug(error); 11 | }); -------------------------------------------------------------------------------- /src/pre.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { ContainerMapping } from './container-mapping'; 3 | import { getExecutor } from './msdo-interface'; 4 | 5 | async function runPre() { 6 | await getExecutor(ContainerMapping).runPreJob(); 7 | } 8 | 9 | runPre().catch((error) => { 10 | core.debug(error); 11 | }); -------------------------------------------------------------------------------- /test/post.tests.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import https from 'https'; 3 | import sinon from 'sinon'; 4 | import * as core from '@actions/core'; 5 | import * as exec from '@actions/exec'; 6 | import { run, sendReport, _sendReport } from '../lib/post'; 7 | 8 | describe('postjob run', function() { 9 | let execStub: sinon.SinonStub; 10 | let sendReportStub: sinon.SinonStub; 11 | 12 | beforeEach(() => { 13 | execStub = sinon.stub(exec, 'exec'); 14 | sendReportStub = sinon.stub(sendReport); 15 | }); 16 | 17 | afterEach(() => { 18 | execStub.restore(); 19 | sendReport.restore(); 20 | }); 21 | 22 | it('should run three docker commands and send the report', async () => { 23 | await run(); 24 | 25 | sinon.assert.callCount(execStub, 3); 26 | sinon.assert.calledWith(execStub, 'docker --version'); 27 | sinon.assert.calledWith(execStub, 'docker images --format CreatedAt={{.CreatedAt}}::Repo={{.Repository}}::Tag={{.Tag}}::Digest={{.Digest}}'); 28 | 29 | sinon.assert.calledOnce(sendReport); 30 | }); 31 | }); 32 | 33 | describe('postjob sendReport', function() { 34 | let _sendReportStub: sinon.SinonStub; 35 | let data: Object; 36 | 37 | beforeEach(() => { 38 | _sendReportStub = sinon.stub(_sendReport); 39 | data = { 40 | "key.fake": "value.fake" 41 | }; 42 | }); 43 | 44 | afterEach(() => { 45 | _sendReportStub.restore(); 46 | }); 47 | 48 | it('should still call _sendReport once if retryCount < 1', async () => { 49 | await sendReport(data, -1); 50 | sinon.assert.calledOnce(_sendReport); 51 | }); 52 | 53 | it('should succeed if _sendReport succeeds', async () => { 54 | _sendReportStub.throws(new Error('_sendReport().Error')); 55 | 56 | await sendReport(data, 0); 57 | sinon.assert.calledOnce(_sendReport); 58 | }); 59 | 60 | it('should succeed if _sendReport succeeds', async () => { 61 | 62 | 63 | await sendReport(data, 0); 64 | sinon.assert.calledOnce(_sendReport); 65 | }); 66 | 67 | // should still call _sendReport once if retryCount < 1 68 | // should succeed if _sendReport succeeds 69 | // should fail if _sendReport fails and retryCount == 0 70 | // should succeed if _sendReport fails the first time and succeeds the second if retryCount > 0 71 | // should fail if _sendReport fails for all retries 72 | 73 | }); 74 | 75 | 76 | describe('postjob _sendReport', function() { 77 | let core_getIDTokenStub: sinon.SinonStub; 78 | let https_requestStub: sinon.SinonStub; 79 | let clientRequestStub; 80 | let data: Object; 81 | const expectedUrl = 'https://dfdinfra-afdendpoint2-dogfood-edb5h5g7gyg7h3hq.z01.azurefd.net/github/v1/container-mappings'; 82 | 83 | beforeEach(() => { 84 | core_getIDTokenStub = sinon.stub(core, 'getIDToken'); 85 | https_requestStub = sinon.stub(https, 'request'); 86 | clientRequestStub = sinon.stub(); 87 | clientRequestStub.end = sinon.stub(); 88 | 89 | core_getIDTokenStub.resolves('bearerToken.mock'); 90 | https_requestStub 91 | .callsArgWith(2, { 92 | on: (event, callback) => { 93 | if (event === 'data') { 94 | callback(); 95 | } else if (event === 'end') { 96 | callback(); 97 | } 98 | }, 99 | end: () => {} 100 | }) 101 | .returns(clientRequestStub); 102 | 103 | data = { 104 | "key.fake": "value.fake" 105 | }; 106 | }); 107 | 108 | afterEach(() => { 109 | core_getIDTokenStub.restore(); 110 | https_requestStub.restore(); 111 | clientRequestStub.restore(); 112 | }); 113 | 114 | it('should still call _sendReport once if retryCount < 1', async () => { 115 | await _sendReport(data, -1); 116 | sinon.assert.calledOnce(core_getIDTokenStub); 117 | sinon.assert.calledOnce(https_requestStub); 118 | 119 | // { 120 | // method: 'POST', 121 | // timeout: 2500, 122 | // headers: { 123 | // 'Content-Type': 'application/json', 124 | // 'Authorization': 'Bearer bearerToken.mock' 125 | // }, 126 | // data: data 127 | // }; 128 | }); 129 | }); -------------------------------------------------------------------------------- /test/pre.tests.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import * as core from '@actions/core'; 3 | import { run } from '../lib/pre'; 4 | 5 | describe('prejob run', () => { 6 | let saveStateStub: sinon.SinonStub; 7 | let dateSub: sinon.SinonStub; 8 | 9 | beforeEach(() => { 10 | saveStateStub = sinon.stub(core, 'saveState'); 11 | dateSub = sinon.stub(global, 'Date'); 12 | }); 13 | 14 | afterEach(() => { 15 | saveStateStub.restore(); 16 | }); 17 | 18 | it('should save the current time as PreJobStartTime', async () => { 19 | dateSub.returns({ 20 | toISOString: () => '2023-01-23T45:12:34.567Z' 21 | }); 22 | 23 | await run(); 24 | 25 | sinon.assert.calledWithExactly(saveStateStub, 'PreJobStartTime', '2023-01-23T45:12:34.567Z'); 26 | }); 27 | }); -------------------------------------------------------------------------------- /test/testCommon.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | export const stagingDirectory = path.join(__dirname, '..', 'lib'); 4 | 5 | export enum TestConstants { 6 | Error = 'Error', 7 | Success = 'Success', 8 | TaskTestTrace = 'TASK_TEST_TRACE', 9 | MockResponse = 'MOCK_RESPONSE', 10 | InputPrefix = 'INPUT_' 11 | }; -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "rootDir": "./", 9 | "strict": false, 10 | "esModuleInterop": true, 11 | "noImplicitAny": false, 12 | "removeComments": true, 13 | "sourceMap": true 14 | }, 15 | "include": [ 16 | "**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "strict": false, 11 | "esModuleInterop": true, 12 | "noImplicitAny": false, 13 | "removeComments": true, 14 | "sourceMap": false 15 | 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.test.ts" 23 | ] 24 | } --------------------------------------------------------------------------------