Wednesday, November 4, 2026
HomePowershellPressure Safety Scans with Azure DevOps Extends Templates

Pressure Safety Scans with Azure DevOps Extends Templates


You added a safety scanning template to the shared library, so that you assume the scan runs. It doesn’t. Groups get a file they’re welcome to name, and nothing breaks once they skip it. Image the pipeline that skipped it: a developer wants a hotfix out, the SCA step is failing on a transitive dependency no person can patch this afternoon, and deleting 4 traces of YAML will get the merge by way of. Six weeks later an auditor asks which repositories ran the scan, and your sincere reply is “those that felt prefer it.”

This tutorial replaces that conference with enforcement. You’ll write an extends template that owns each pipeline’s outer construction, level an utility pipeline at it, and make the scan fail the construct. Then you’ll add a Required template test to the service connection, so skipping the template means no deployment. Rollout and audit proof come final, since enforcement you can’t show is only a rumor.

Stipulations for Forcing a Scan

If you wish to observe alongside, you’ll want:

  • An Azure DevOps mission the place you’ll be able to create YAML pipelines and decide to Git. Two repositories make the possession boundary apparent; two folders work.

  • Undertaking Administrator on that mission, as a result of enhancing checks on a protected useful resource wants the Administrator or Creator function there, a separate grant from pipeline edit rights, because the Azure DevOps permission reference paperwork.

  • The Microsoft Safety DevOps extension if you’d like MSDO because the scanner. Putting in an organization-level extension wants Undertaking Assortment Administrator, so plan that decision.

  • A service connection to any Azure subscription if you wish to watch the gate reject an actual deployment. The test works on an setting with out one.

Extends templates, checks, and department insurance policies ship with Azure DevOps Companies. Examined on Azure DevOps Companies in October 2026 with MicrosoftSecurityDevOps@1 on windows-latest.

Why an Included Template Enforces Nothing

An included YAML pipeline template is a compiler-assisted copy and paste. The template: key phrase is legitimate inside levels, jobs, steps, and variables, and the included file lands there. That makes it wonderful at eradicating duplication and ineffective at mandating something. The consuming pipeline retains possession of its outer construction, so it might put steps earlier than the embody, after it, or nowhere.

Attribute Embody template Extends template
Who controls the outer construction The appliance pipeline The template creator
The place customized steps could go Wherever within the file Solely at declared parameter factors
What occurs when a staff ignores it Nothing No entry to protected assets

An extends template inverts that relationship. It defines the pipeline skeleton: levels, order, and the slots the place a staff could inject construct steps, whereas the applying file turns into a parameter payload. Microsoft’s safety steering states the intent plainly: “For probably the most safe pipelines, begin through the use of extends templates. These templates outline the outer construction of the pipeline and assist stop malicious code infiltration.”

A compiled pipeline is bounded too. The imposed limits on template updates reject a run with greater than 100 separate YAML information, greater than 100 ranges of template nesting, or greater than 20 MB of YAML parse reminiscence, so nested contains cease at a set depth.

The catch is that extends provides you construction, not compliance. A staff can nonetheless write a recent pipeline that by no means mentions your template. Enforcement wants a second mechanism, which Step 4 provides.

Step 1: Creator the Azure DevOps Extends Template

Create templates/security-extends.yml within the platform repository. The file should be a legitimate pipeline by itself, as a result of each consuming pipeline inherits its levels, because the extends schema reference requires. Begin with parameters, the one floor a staff could contact.

# templates/security-extends.yml
parameters:
- identify: buildPool
  kind: string
  default: Azure Pipelines
  values:
  - Azure Pipelines
  - private-pool-1
- identify: buildSteps
  kind: stepList
  default: []

levels:
- stage: Safety
  jobs:
  - job: scan
    pool:
      vmImage: windows-latest
    steps:
    - job: MicrosoftSecurityDevOps@1
      displayName: 'Scan IaC and containers'
      inputs:
        classes: 'IaC,containers'
        break: true
        publish: true
        artifactName: 'CodeAnalysisLogs'

A string parameter constrained by values rejects any agent pool you didn’t record, so no person factors the construct at an unvetted pool, and the scan job runs on a Microsoft-hosted windows-latest picture reasonably than a staff’s pool. stepList is the typed gap the place a staff’s steps go. Template parameters develop into constants earlier than the pipeline runs, so a step that doesn’t match the declared form fails compilation reasonably than at deployment.

Make the Scan Stage a Laborious Dependency

The construct stage is determined by the scan. succeeded() is already the default situation, and writing it out retains the intent seen after the following well-meaning edit.

- stage: Construct
  dependsOn: Safety
  situation: succeeded()

A failed scan job stops the pipeline earlier than something compiles. Scan-after-build nonetheless blocks a deployment, however it burns construct minutes first and leaves you guessing which artifact was rejected.

Wrap the Construct Steps As a substitute of Trusting Them

Iterate the staff’s steps, move each key by way of untouched, and reject job varieties that hand arbitrary code execution to whoever wrote the YAML.

  jobs:
  - job: construct
    pool: ${{ parameters.buildPool }}
    steps:
    - script: 'echo "Base: pre-build"'
      displayName: 'Base: Pre-build'
    - ${{ every step in parameters.buildSteps }}:
      - ${{ every pair in step }}:
          ${{ if notIn(pair.worth, 'CmdLine@2', 'PowerShell@2', 'Bash@3') }}:
            ${{ pair.key }}: ${{ pair.worth }}
          ${{ if in(pair.worth, 'CmdLine@2', 'PowerShell@2', 'Bash@3') }}:
            '${{ pair.worth }}': error
    - script: 'echo "Base: post-build"'
      displayName: 'Base: Publish-build'

That '${{ pair.worth }}': error line is authorized YAML and an unlawful step. Azure palms a - script: step to the loop as a CmdLine@2 job, so blocking one writes a property named CmdLine@2 with the worth error. The validator rejects the file on the schema degree with Surprising worth 'CmdLine@2', and compilation stops earlier than an agent picks up the job, which is the correct consequence for a pipeline working shell instructions by way of governance. Microsoft’s restrict-by-task-type instance runs the identical loop to reject a CmdLine@2 step.

Right here is the entire file, with the indentation resolved:

# templates/security-extends.yml
parameters:
- identify: buildPool
  kind: string
  default: Azure Pipelines
  values:
  - Azure Pipelines
  - private-pool-1
- identify: buildSteps
  kind: stepList
  default: []

levels:
- stage: Safety
  jobs:
  - job: scan
    pool:
      vmImage: windows-latest
    steps:
    - job: MicrosoftSecurityDevOps@1
      displayName: 'Scan IaC and containers'
      inputs:
        classes: 'IaC,containers'
        break: true
        publish: true
        artifactName: 'CodeAnalysisLogs'
- stage: Construct
  dependsOn: Safety
  situation: succeeded()
  jobs:
  - job: construct
    pool: ${{ parameters.buildPool }}
    steps:
    - script: 'echo "Base: pre-build"'
      displayName: 'Base: Pre-build'
    - ${{ every step in parameters.buildSteps }}:
      - ${{ every pair in step }}:
          ${{ if notIn(pair.worth, 'CmdLine@2', 'PowerShell@2', 'Bash@3') }}:
            ${{ pair.key }}: ${{ pair.worth }}
          ${{ if in(pair.worth, 'CmdLine@2', 'PowerShell@2', 'Bash@3') }}:
            '${{ pair.worth }}': error
    - script: 'echo "Base: post-build"'
      displayName: 'Base: Publish-build'

That compiles to 2 levels, Safety then Construct, with buildSteps between the bottom steps.

Compiled pipeline stages


Warning: Compile-time rejection is unforgiving by design. As soon as Bash@3 and CmdLine@2 are blocked, a staff that wants a shell step has no workaround inside its personal YAML. Expose the operations they want as named duties within the template, or the primary blocked pipeline can be adopted by a request to delete the test.


Step 2: Prolong It From an Software Pipeline

The appliance repository holds a pipeline that’s principally a parameter payload. Every little thing structural lives within the template it references.

# azure-pipelines.yml
set off:
- most important

assets:
  repositories:
  - repository: templates
    kind: git
    identify: Platform/PlatformTemplates
    ref: refs/tags/v1.4.0

extends:
  template: templates/security-extends.yml@templates
  parameters:
    buildPool: private-pool-1
    buildSteps:
    - job: NodeTool@0
      inputs:
        versionSpec: '22.x'
    - job: Npm@1
      displayName: 'Restore packages'
      inputs:
        command: ci
    - job: Npm@1
      displayName: 'Run checks'
      inputs:
        command: customized
        customCommand: check

Each step in that payload is a named job on goal. script is the YAML shortcut for CmdLine@2, so a - script: line reaches the loop as the duty kind it blocks and fails compilation.

Three particulars carry the worth. The @templates suffix resolves by way of the assets.repositories alias, the way in which a pipeline reaches a template in one other repository. The ref pins a tag, so a nasty template commit can’t change each pipeline the second it merges. And set off stays within the client file, as a result of a set off declared inside an extends template is ignored with out warning.

Maintain Compile-Time and Runtime Expressions Straight

The ${{ }} syntax above is a compile-time expression, evaluated when the YAML turns into a plan, with entry to parameters and statically outlined variables. The $[ ] syntax is a runtime expression, evaluated through the run with extra variables out there and no entry to parameters. A $[ ] expression inside a parameter block is not going to work as anticipated, as a result of the worth was already resolved to a relentless.

templateContext passes further properties into jobs a template consumes as parameters. Declare the parameter as jobList, then learn ${{ job.templateContext.<property> }}, because the template parameters documentation exhibits. That provides you per-job metadata with out a free-form variable a pipeline creator might overwrite.

Step 3: Make the Scan Fail the Construct

A scanner that publishes findings and exits 0 is a report, and experiences get ignored. MSDO decides this with one enter; the analyzer classes determine what that enter catches.

Enter What it controls Why pin it within the template
classes Analyzer households: code, artifacts, IaC, containers Groups scope right down to the class that passes quickest
break Fails the step on excessive severity outcomes Defaults to false, so an unpinned scan by no means blocks
instruments Which analyzers run, reminiscent of checkov or trivy Leaves a spot when a staff disables the sluggish scanner

The break enter decides whether or not the scan blocks something in any respect. It defaults to false, so a template that omits the enter produces a inexperienced construct with essential findings hooked up. break: true fails the step on any excessive severity end result, and the Step 1 situation turns that right into a stopped pipeline.


Actuality Verify: break: true reacts to excessive severity findings solely. A medium-severity container vulnerability is not going to fail the step. In case your coverage says medium blocks a launch, the scan step just isn’t your enforcement level and a coverage analysis has to hold that rule.


One accuracy notice older guides miss: MSDO’s built-in secrets and techniques scanning by way of CredScan is deprecated, changed by GitHub Superior Safety for Azure DevOps. Settings past these defaults are documented within the Microsoft Safety DevOps wiki. A scan that fails the construct nonetheless governs solely the pipelines that decision the template, so Step 4 provides the test for the remaining.

Step 4: Implement It With a Required Template Verify

The Required template test is what turns the template right into a rule. Microsoft’s checks documentation places the habits in a single sentence: “With the required template test, you’ll be able to implement pipelines to make use of a particular YAML template. When this test is in place, a pipeline fails if it doesn’t lengthen from the referenced template.”

The test sits on a protected useful resource, so a pipeline creator enhancing the YAML can’t modify what runs earlier than a stage begins.

Place the Verify on the Credentials

Put the Required template test on the service connection that holds your deployment credentials, as a result of the credentials are the precise boundary.

  1. Open the mission, then Pipelines > Service connections, and choose the connection to guard.

  2. Select Approvals and Checks from the menu subsequent to Edit.

  3. Choose Required template, then fill in Repository kind, Repository, Ref, and Path to required template. For this construct: Azure Repos, Platform/PlatformTemplates, refs/tags/v1.4.0, templates/security-extends.yml.

  4. Save the test and make sure it seems.

Static checks run first, alongside department management and artifact analysis, so a non-compliant pipeline fails earlier than an approver is paged. A number of required templates can sit on one connection, so you’ll be able to mandate a safety base and a deployment wrapper individually. The panel under exhibits the finished fields for this construct.

Required template check

Step 5: Shut the Bypass Routes Groups Will Discover

Enforcement holds for a couple of week earlier than somebody finds a method round it. Every bypass has a management that closes it.

Bypass try Management that closes it
A pipeline that by no means mentions the template Required template test on the service connection
A shell step smuggled into the step record Blocked job varieties within the compile-time loop
A job you by no means authorised Permit-list on step.job as an alternative of a blocklist
Pointing at an unvetted agent pool values record on the pool parameter
Modifying the template straight on most important Department insurance policies with reviewers and construct validation
A brand new service connection to dodge the test Limit who can create service connections

Commerce the Blocklist for an Permit-Checklist

The allow-list model inverts the check onto authorised job names. in and notIn are documented expression features, so the form stays the identical.

    - ${{ every step in parameters.buildSteps }}:
      - ${{ if notIn(step.job, 'NodeTool@0', 'Npm@1', 'DotNetCoreCLI@2', 'PublishBuildArtifacts@1') }}:
        - '${{ step.job }}': error
      - ${{ else }}:
        - ${{ step }}

Two holes survive an allow-list. It checks step.job by identify, so a step written as - script: | is judged by the duty identify Azure assigns it reasonably than the shell key it carries; check pair.key in opposition to script, bash, pwsh, and powershell to power groups onto named duties. And an authorised job with a harmful enter continues to be authorised: DotNetCoreCLI@2 runs no matter arguments string a staff palms it. Deal with the allow-list as one management amongst a number of reasonably than full protection.


Key Perception: Defend the template repository like manufacturing. The test verifies {that a} pipeline extends a template at a pinned ref, so whoever can merge to that tag controls each ruled pipeline. Require two reviewers who aren’t the creator, require a linked work merchandise, and run pattern client pipelines in opposition to each change.


Rolling It Out With out Stalling Supply

Don’t flip the test onto your manufacturing service connection on a Monday and go to lunch. Rise up a second connection that solely the pilot staff can use, level a handful of consultant pipelines on the template, and depart the previous connection ungoverned. Protection grows one reversible migration at a time.

Pin the template by tag and bump the tag on goal. A staff on v1.4.0 retains working whereas v1.5.0 is examined in opposition to the pilot set, which turns a breaking change right into a scheduled occasion. The template repository itself wants the usual protections: a minimal reviewer depend, no self-approval, remark decision, and construct validation so a syntax error by no means reaches a tag. Grandfather what can’t transfer this quarter in a written exception record with an proprietor and an expiry date, the place auditors can learn it.

Verifying Enforcement and Proving It to Auditors

Take a look at the gate the way in which an attacker would. Create a throwaway pipeline that deploys to the protected useful resource with out an extends block, run it, and make sure the stage fails on the test reasonably than on the deployment. Then create one which extends the template and make sure it passes. Two runs let you know whether or not the rule is actual.

For the audit file, acquire three artifacts. The test configuration on the service connection is the rule itself, and it lives outdoors YAML the place a pipeline creator can’t edit it. Run historical past exhibits the checks panel for every deployment, together with bypasses, which want administrator permission and are recorded in opposition to the identification that used them. The CodeAnalysisLogs artifact from every scan is proof {that a} scan ran, and Defender for Cloud ingests that artifact for steady visibility into repositories with unresolved findings.

Do this and the auditor’s query modifications form. It stops being “did anybody skip the scan” and turns into “which exception continues to be open and who owns it,” which you’ll be able to reply from a listing as an alternative of a reminiscence.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments