Detection rules › Kusto

External Upstream Source Added to Azure DevOps Feed

Status
available
Severity
medium
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'The detection looks for new external sources added to an Azure DevOps feed. An allow list can be customized to explicitly allow known good sources. An attacker could look to add a malicious feed in order to inject malicious packages into a build pipeline.'

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Rule body

id: adc32a33-1cd6-46f5-8801-e3ed8337885f
name: External Upstream Source Added to Azure DevOps Feed
description: |
  'The detection looks for new external sources added to an Azure DevOps feed. An allow list can be customized to explicitly allow known good sources. 
  An attacker could look to add a malicious feed in order to inject malicious packages into a build pipeline.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1199
query: |
  // Add any known allowed sources and source locations to the filter below (the NuGet Gallery has been added here as an example).
  let allowed_sources = dynamic(["NuGet Gallery"]);
  let allowed_locations = dynamic(["https://api.nuget.org/v3/index.json"]);
  ADOAuditLogs
  // Look for feeds created or modified at either the organization or project level
  | where OperationName matches regex "Artifacts.Feed.(Org|Project).Modify"
  | where Details has "UpstreamSources, added"
  | extend FeedName = tostring(Data.FeedName)
  | extend FeedId = tostring(Data.FeedId)
  | extend UpstreamsAdded = Data.UpstreamsAdded
  // As multiple feeds may be added expand these out
  | mv-expand UpstreamsAdded
  // Only focus on external feeds
  | where UpstreamsAdded.UpstreamSourceType !~ "internal"
  | extend SourceLocation = tostring(UpstreamsAdded.Location)
  | extend SourceName = tostring(UpstreamsAdded.Name)
  // Exclude sources and locations in the allow list
  | where SourceLocation !in (allowed_locations) and SourceName !in (allowed_sources)
  | extend SourceProtocol = tostring(UpstreamsAdded.Protocol)
  | extend SourceStatus = tostring(UpstreamsAdded.Status)
  | project-reorder TimeGenerated, OperationName, ScopeDisplayName, ProjectName, FeedName, SourceName, SourceLocation, SourceProtocol, ActorUPN, UserAgent, IpAddress
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let allowed_sources = dynamic(["NuGet Gallery"]);
let allowed_locations = dynamic(["https://api.nuget.org/v3/index.json"]);

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName matches regex "Artifacts.Feed.(Org|Project).Modify"

Stage 3: where

| where Details has "UpstreamSources, added"

Stage 4: extend (3 consecutive steps)

| extend FeedName = tostring(Data.FeedName)
| extend FeedId = tostring(Data.FeedId)
| extend UpstreamsAdded = Data.UpstreamsAdded

Stage 5: mv-expand

| mv-expand UpstreamsAdded

Stage 6: where

| where UpstreamsAdded.UpstreamSourceType !~ "internal"

Stage 7: extend

| extend SourceLocation = tostring(UpstreamsAdded.Location)

Stage 8: extend

| extend SourceName = tostring(UpstreamsAdded.Name)

Stage 9: where

| where SourceLocation !in (allowed_locations) and SourceName !in (allowed_sources)

Stage 10: extend

| extend SourceProtocol = tostring(UpstreamsAdded.Protocol)

Stage 11: extend

| extend SourceStatus = tostring(UpstreamsAdded.Status)

Stage 12: project-reorder

| project-reorder TimeGenerated, OperationName, ScopeDisplayName, ProjectName, FeedName, SourceName, SourceLocation, SourceProtocol, ActorUPN, UserAgent, IpAddress

Stage 13: extend

| extend timestamp = TimeGenerated

Stage 14: extend

| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SourceLocationeqallowed_locationsexcludes:SourceLocation field:"SourceLocation" value:"allowed_locations"
SourceNameeqallowed_sourcesexcludes:SourceName field:"SourceName" value:"allowed_sources"

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
FeedNameextend
FeedIdextend
UpstreamsAddedextend
SourceLocationextend
SourceNameextend
SourceProtocolextend
SourceStatusextend
timestampextend
AccountNameextend
AccountUPNSuffixextend