PHP2Shell hunting: queries, results and additional value
GhostPaw story / Upstream hunting pack
HISTORICAL REVIEWED CAPTURE / 2026-10-24T18:35:01.215Z to 2026-10-24T18:35:03.883Z.
Independent source logs versus actual observations under existing enabled plans; not a Defender-disabled experiment. Read-only, exact lab, retrospective 24-hour queries. No scan, exploit, image execution, DCR/rule deployment, protection change or tenant-wide XDR query. Original operator login/test deadline:2026-10-24T18:45:04.967Z. This public recording remains historical and read-only; no execution window was extended.
Queries below are reviewed templates; actual resource identifiers and raw sensitive fields are not public. Row counts are actual; null means not executed, never zero matches.
Twenty additional-value walkthroughs
row-01 / Defender TVM CVE exposure
Requires: TVM inventory and published CVE detection
- Record actual CVE/device rows and collection scope.
- If empty or unpublished, continue inventory, bundled-product and worker discovery; never pass as safe.
tvm-exposure
row-02 / Defender for Cloud VM and running-container posture
Requires: Applicable Defender plan, permissions and actual assessment coverage
- Inspect real assessment status, resource identity, affected package and advisory.
- Review available Cloud Security Explorer and attack-path evidence without inventing relationships.
php-assessments
row-03 / App Service and Function runtime discovery
Requires: Approved Resource Graph scope; per-app configuration read if fields are missing
- Review all runtime strings and empty configuration, including slots and custom containers.
- Verify the effective patch level separately; major.minor configuration never proves a patch.
appservice-runtimes / appservice-runtime-discovery
row-04 / Registry, running-image and zero-finding SBOM inventory
Requires: Registry assessment coverage and separately authorized image/SBOM access
- Correlate actual findings by registry, repository and immutable digest.
- Manually inventory deployed image digests and inspect SBOMs, including source-built PHP and zero-finding images.
- Do not execute an untrusted image or suppress SBOM/pull failures.
container-cve-findings / container-php-findings
row-05 / MDE inventory, fixed-version and distinct-software review
Requires: MDE software inventory and supported process telemetry for host discovery
- Compare runtime, not application, versions to the correct upstream branch.
- Review vendor backports, prereleases, unparsed and unsupported branches manually.
- Review distinct PHP names and all packages on observed web hosts for unlisted stacks.
php-inventory / software-discovery
row-06 / Defender EASM external exposure
Requires: Authorized EASM inventory export
- Review approved/live PHP frameworks, web servers, PHP-based components and IP banners.
- Review distinct frameworks, components and server values beyond fixed keyword lists.
- Preserve asset identity and export time; hidden PHP and internal assets remain gaps.
Walkthrough only: separate authorization/evidence required; no scanner or mutation is executed.
row-07 / Code search and DevOps ownership mapping
Requires: Explicit repository scope and authorized code-search/DevOps access
- Search PHP base images, package installs, Compose/Kubernetes and composer.json.
- Review distinct repositories and map image digest to repository, pipeline and accountable owner.
- Record search caps and inaccessible repositories; DevOps mapping is not free-text search.
Walkthrough only: separate authorization/evidence required; no scanner or mutation is executed.
row-08 / Bundled PHP binaries and reviewed NCSC product leads
Requires: TVM software evidence and reviewed local NCSC/vendor product rows
- Prepare nonempty local product input and preserve feed revision/date, source and advisory status.
- Treat product-name matches and beta disk evidence as leads; confirm the actual serving runtime.
- Inventory gaps, stale feed entries and an empty product template cannot pass as completed coverage.
php-binary-evidence / bundled-products
row-09 / Worker observations, mod_php proxy and unknown launchers
Requires: MDE inventory, process starts and supported inbound network observations
- Prioritize Apache plus module-package observations without claiming the module is loaded.
- Retain installed-but-quiet hosts and review unfamiliar PHP launchers/inbound processes.
- Check long-lived workers outside retention; no recent start does not mean stopped or absent.
worker-classification / worker-discovery
row-10 / Optional authorized network fingerprint gap review
Requires: Separate written scan scope, approved scanner/network reachability and SOC coordination
- Review separately collected approved fingerprints and distinct technologies/server banners.
- Confirm leads with runtime/inventory evidence; absent PHP headers are not clearance.
- No scanner, arbitrary target or exploit is run by this dojo.
Walkthrough only: separate authorization/evidence required; no scanner or mutation is executed.
hunt-A / Prioritize exposed high-risk worker candidates
Requires: Row 09 classification and Row 06 approved/live exposure export
- Cross-match stable asset/owner identifiers, not hostname similarity alone.
- Prioritize class 1/2 exposed candidates and retain unmatched/uncertain mappings for review.
Walkthrough only: separate authorization/evidence required; no scanner or mutation is executed.
hunt-B / Web worker children and legitimate baseline
Requires: Supported MDE process telemetry and reviewed worker-name discovery
- Review shell, interpreter, downloader and discovery children with owner-approved baselines.
- Validate shared worker-name coverage after discovery; in-worker execution remains a blind spot.
worker-child-processes / worker-baseline
hunt-C / Syslog crashes, OOM and worker churn
Requires: Linux AMA, Syslog DCR and actual application error routing
- Verify collection by host, facility and severity before interpreting results.
- Review crash/memory/churn events and five-event 10-minute bins against health/load baselines.
syslog-worker-failures
hunt-D1 / App Service HTTP error, session and cookie leads
Requires: AppServiceHTTPLogs in the intended workspace
- Review five 5xx POSTs plus request size over 100000 bytes per five-minute resource/host/client bin.
- Review short POST/session/error clusters and request-cookie length over 1000.
- Do not infer Content-Type, response cookies or an ordered exploit from these fields.
appservice-http
hunt-D2 / Application Gateway WAF and backend correlation
Requires: Resource-specific AGWAccessLogs and AGWFirewallLogs, not Front Door logs
- Review POST/backend-error bursts and generic request-body/multipart rule leads.
- Verify deployed ruleset mappings and correlate only nonempty transaction IDs within one gateway.
- An unjoined firewall event does not establish backend success or failure.
appgateway-waf
hunt-E1 / PHP file writes and rapid deletion
Requires: Supported MDE file telemetry
- Review worker/temp-path PHP-family writes and deletion within the inclusive following 10 minutes.
- Check device, full path, hash and timestamps; CMS updates and path reuse are alternative explanations.
php-file-drops
hunt-E2 / Worker and child-tool public network activity
Requires: Supported MDE network telemetry
- Review direct workers on unusual public ports and child tools on any public port including 80/443.
- Review excluded common-port direct traffic and private/unknown IP-type destinations separately.
worker-network
hunt-F1 / AKS ContainerLogV2 worker errors
Requires: Container Insights V2 and actual PHP/FPM stdout/stderr output
- Verify namespace, pod, container and resource coverage and worker-output configuration.
- Review memory, timeout, signal/churn and session diagnostics as leads, not exploitation proof.
container-worker-errors
hunt-F2 / Custom web error and privacy-preserving access logs
Requires: Custom WebErrorLog_CL/WebAccessLog_CL tables, AMA DCR and the documented access format
- Verify collection identity, nine-field parsing and explicit malformed-record counts.
- Review multipart errors, chunked multipart and response-cookie size flags without collecting cookie values.
- A '-' cookie flag is unobserved, not a negative result.
web-text-logs
hunt-G / Patch, partial mitigations and closure evidence
Requires: Verified PHP/vendor advisories, authorized change owner and effective web-SAPI configuration
- Verify the deployed vendor build or image digest contains the correct patch and restart all workers.
- Review session upload-progress, PHP 8.5 backtraces, decoded-body limits and advisory mitigation trade-offs; do not blindly remove memory limits.
- Never use RLIMIT_AS/RLIMIT_DATA as this mitigation; WAF markers and synthetic remediation do not close a real CVE.
Walkthrough only: separate authorization/evidence required; no scanner or mutation is executed.
lab-waf
lab-workspace / detections/waf-requests.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Real Front Door records after diagnostics ingestion; benign marker only.
Required: At least one declared optional lab source.
Reviewed query template
// Required when available: AzureDiagnostics (FrontDoorWebApplicationFirewallLog).
// The empty source keeps a new workspace queryable when the table is missing.
// Real WAF records of benign input are NOT a CVE detection or proof of compromise.
let RunIdFilter = "";
let Window = 1h;
let Empty = datatable(TimeGenerated:datetime)[];
union isfuzzy=true Empty, AzureDiagnostics
| where TimeGenerated between (ago(Window) .. now())
| where tostring(column_ifexists("Category", "")) == "FrontDoorWebApplicationFirewallLog"
| extend RequestUri = tostring(column_ifexists("requestUri_s", ""))
| extend RunId = extract(@"[?&]runId=([0-9a-f-]{36})", 1, RequestUri)
| where isnotempty(RunId) and (isempty(RunIdFilter) or RunId == RunIdFilter)
| project TimeGenerated, RunId,
ResourceId = tostring(column_ifexists("_ResourceId", "")),
ClientIp = tostring(column_ifexists("clientIP_s", "")),
Hostname = tostring(column_ifexists("host_s", "")),
Action = tostring(column_ifexists("action_s", "")),
RuleName = tostring(column_ifexists("ruleName_s", "")),
TrackingReference = tostring(column_ifexists("trackingReference_s", "")),
RequestUri, EvidenceKind = "real-waf-log-benign-input"
| order by TimeGenerated asc
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-waf",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "207d566fc3a218dd68db611684c7bc4222fee31886268297ed496a9b84094b36"
}
lab-php-errors
lab-workspace / detections/php-errors.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Synthetic examples are labelled separately from actual PHP log messages.
Required: At least one declared optional lab source.
Reviewed query template
// Required when available: AppServiceConsoleLogs or AppServiceAppLogs.
// Synthetic php-error events are deliberately NOT real PHP exceptions.
let RunIdFilter = "";
let Empty = datatable(TimeGenerated:datetime)[];
union isfuzzy=true Empty, AppServiceConsoleLogs, AppServiceAppLogs
| where TimeGenerated between (ago(1h) .. now())
| extend Message = tostring(column_ifexists("ResultDescription", column_ifexists("Message", "")))
| extend Event = parse_json(extract(@"DOJO_EVENT (\{.*\})", 1, Message))
| where tostring(Event.signal) == "php-error" or Message has_any ("PHP Fatal error", "PHP Warning", "PHP Parse error")
| extend RunId = tostring(Event.runId), Synthetic = tobool(Event.synthetic)
| where isempty(RunIdFilter) or RunId == RunIdFilter
| project TimeGenerated, RunId, Synthetic,
ResourceId = coalesce(tostring(Event.resourceId), tostring(column_ifexists("_ResourceId", ""))),
Hostname = tostring(Event.hostname), ClientIp = tostring(Event.clientIp),
Identity = tostring(Event.identity), Message,
EvidenceKind = iff(Synthetic == true, "synthetic-php-error", "observed-php-error-review-required")
| order by TimeGenerated asc
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-php-errors",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "72375d860f92ab629466f75ae40ee053d854c2581cd7b8446fd54b3f5e833872"
}
lab-child-process
lab-workspace / detections/unexpected-child-process.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Synthetic app logs only; MDE records require a separately supported workload/ingestion path.
Required: At least one declared optional lab source.
Reviewed query template
// Optional: DeviceProcessEvents, only for a workload with supported MDE telemetry.
// App Service in this scaffold does NOT emit DeviceProcessEvents.
// Required for synthetic examples: AppServiceConsoleLogs.
// Maintenance and legitimate workers can match; an event is not compromise.
let RunIdFilter = "";
let Empty = datatable(TimeGenerated:datetime)[];
union isfuzzy=true withsource=SourceTable Empty, DeviceProcessEvents, AppServiceConsoleLogs
| where TimeGenerated between (ago(1h) .. now())
| extend Event = parse_json(extract(@"DOJO_EVENT (\{.*\})", 1, tostring(column_ifexists("ResultDescription", ""))))
| extend ParentProcess = tostring(column_ifexists("InitiatingProcessFileName", "")),
ChildProcess = tostring(column_ifexists("FileName", "")), Synthetic = tobool(Event.synthetic)
| where (SourceTable endswith "DeviceProcessEvents"
and ParentProcess in~ ("php", "php-fpm", "nginx", "httpd", "apache2")
and ChildProcess !in~ ("php", "php-fpm", "nginx", "httpd", "apache2"))
or (Synthetic == true and tostring(Event.signal) == "process-synthetic")
| extend RunId = tostring(Event.runId)
| where isempty(RunIdFilter) or RunId == RunIdFilter
| project TimeGenerated, RunId, Synthetic, ParentProcess,
ChildProcess = iff(Synthetic == true, "(synthetic; no child process launched)", ChildProcess),
Hostname = coalesce(tostring(Event.hostname), tostring(column_ifexists("DeviceName", ""))),
DeviceId = tostring(column_ifexists("DeviceId", "")),
ResourceId = coalesce(tostring(Event.resourceId), tostring(column_ifexists("_ResourceId", ""))),
ClientIp = tostring(Event.clientIp),
Identity = coalesce(tostring(Event.identity), tostring(column_ifexists("AccountName", ""))),
EvidenceKind = iff(Synthetic == true, "synthetic-process-shape", "observed-process-review-required")
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-child-process",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "9969d6f9aba9c93ec0dad63e89f9bfa29eca13cd9abad606cd3a1f99f45d51bf"
}
lab-outbound
lab-workspace / detections/unexpected-outbound.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Synthetic app logs only; no outbound connection is made.
Required: At least one declared optional lab source.
Reviewed query template
// Optional: DeviceNetworkEvents, only when supported workload telemetry exists.
// App Service in this scaffold does NOT emit DeviceNetworkEvents.
// Required for synthetic examples: AppServiceConsoleLogs. No egress is simulated.
let RunIdFilter = "";
let Empty = datatable(TimeGenerated:datetime)[];
union isfuzzy=true withsource=SourceTable Empty, DeviceNetworkEvents, AppServiceConsoleLogs
| where TimeGenerated between (ago(1h) .. now())
| extend Event = parse_json(extract(@"DOJO_EVENT (\{.*\})", 1, tostring(column_ifexists("ResultDescription", ""))))
| extend Process = tostring(column_ifexists("InitiatingProcessFileName", "")), Synthetic = tobool(Event.synthetic)
| where (SourceTable endswith "DeviceNetworkEvents" and Process in~ ("php", "php-fpm", "nginx", "httpd", "apache2"))
or (Synthetic == true and tostring(Event.signal) == "outbound-synthetic")
| extend RunId = tostring(Event.runId)
| where isempty(RunIdFilter) or RunId == RunIdFilter
| project TimeGenerated, RunId, Synthetic, Process,
Hostname = coalesce(tostring(Event.hostname), tostring(column_ifexists("DeviceName", ""))),
DeviceId = tostring(column_ifexists("DeviceId", "")),
ResourceId = coalesce(tostring(Event.resourceId), tostring(column_ifexists("_ResourceId", ""))),
Identity = coalesce(tostring(Event.identity), tostring(column_ifexists("InitiatingProcessAccountName", ""))),
ClientIp = tostring(Event.clientIp), RemoteIp = tostring(column_ifexists("RemoteIP", "")),
RemotePort = toint(column_ifexists("RemotePort", int(null))),
EvidenceKind = iff(Synthetic == true, "synthetic-egress-shape-no-connection", "observed-network-review-required")
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-outbound",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "5f03049dfdfb019bffce4af45a4c1182a0533ed0849b08db9c61570d3a1de987"
}
lab-canary
lab-workspace / detections/canary-access.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Fabricated constant fixture, not Azure data-plane access.
Required: At least one declared optional lab source.
Reviewed query template
// Required when available: AppServiceConsoleLogs.
// The canary is an in-code fabricated fixture, NOT a secret or Azure data read.
// A missing RequestAt can be expected when WAF Prevention blocked the marker.
let RunIdFilter = "";
let Empty = datatable(TimeGenerated:datetime)[];
union isfuzzy=true Empty, AppServiceConsoleLogs
| where TimeGenerated between (ago(1h) .. now())
| extend Event = parse_json(extract(@"DOJO_EVENT (\{.*\})", 1, tostring(column_ifexists("ResultDescription", ""))))
| where tobool(Event.synthetic) == true and tostring(Event.signal) in ("request", "canary")
| extend RunId = tostring(Event.runId), Signal = tostring(Event.signal)
| where isnotempty(RunId) and (isempty(RunIdFilter) or RunId == RunIdFilter)
| summarize RequestAt = minif(TimeGenerated, Signal == "request"),
CanaryAt = minif(TimeGenerated, Signal == "canary"),
CanaryIds = make_set(tostring(Event.canary.id)), Identities = make_set(tostring(Event.identity)),
ClientIps = make_set(tostring(Event.clientIp)), Hostnames = make_set(tostring(Event.hostname))
by RunId, ResourceId = tostring(Event.resourceId)
| where isnotnull(CanaryAt)
| extend TimeGenerated = CanaryAt, RequestBeforeCanary = RequestAt <= CanaryAt,
EvidenceKind = "synthetic-fixture-access-not-cloud-data-access"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-canary",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "bd434928e3c70f63fa4d6d9c3277d5806367390568a74b2860bf94fb546bf5f2"
}
lab-correlation
lab-workspace / detections/correlation.kql
Without optional protection: Actual WAF/application logs can support manual investigation independently of optional Defender plans.
Additional value: Supported Defender detections and actual posture add investigation context. Synthetic process, egress and ledger fixtures do not demonstrate a workload detection.
Telemetry: Real WAF plus multiple synthetic signals; posture export is not configured.
Required: At least one declared optional lab source.
Reviewed query template
// Available sources: AzureDiagnostics, AppServiceConsoleLogs.
// Optional exported posture source: SecurityRecommendation (not enabled here).
// All tables are optional at query resolution; an empty workspace returns no rows.
// Replace __APP_RESOURCE_ID__ for hunting; Bicep substitutes it for analytics.
// Posture counts are assessment rows to REVIEW, not verified vulnerability/exposure.
// Synthetic process/egress/canary logs are not actual runtime telemetry or compromise.
let AppResourceId = "__APP_RESOURCE_ID__";
let RunIdFilter = "";
let Empty = datatable(TimeGenerated:datetime)[];
let Sources = union isfuzzy=true withsource=SourceTable Empty, AzureDiagnostics, AppServiceConsoleLogs, SecurityRecommendation
| where TimeGenerated between (ago(1h) .. now());
let AppEvents = Sources
| where SourceTable endswith "AppServiceConsoleLogs"
| extend Event = parse_json(extract(@"DOJO_EVENT (\{.*\})", 1, tostring(column_ifexists("ResultDescription", ""))))
| where tobool(Event.synthetic) == true and tostring(Event.resourceId) =~ AppResourceId
| extend RunId = tostring(Event.runId)
| where isnotempty(RunId) and (isempty(RunIdFilter) or RunId == RunIdFilter)
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated),
LastIngested = max(ingestion_time()), SyntheticSignals = make_set(tostring(Event.signal)),
Hostnames = make_set(tostring(Event.hostname)), ClientIps = make_set(tostring(Event.clientIp)),
Identities = make_set(tostring(Event.identity)) by RunId
| where array_length(SyntheticSignals) >= 2;
let Waf = Sources
| where tostring(column_ifexists("Category", "")) == "FrontDoorWebApplicationFirewallLog"
| extend RequestUri = tostring(column_ifexists("requestUri_s", ""))
| where RequestUri contains "dojoProbe=waf-marker"
| extend RunId = extract(@"[?&]runId=([0-9a-f-]{36})", 1, RequestUri)
| where isnotempty(RunId)
| summarize WafEvents = count(), WafActions = make_set(tostring(column_ifexists("action_s", ""))),
WafResourceIds = make_set(tostring(column_ifexists("_ResourceId", ""))) by RunId;
let Posture = Sources
| where SourceTable endswith "SecurityRecommendation"
| extend AssessmentResource = tostring(column_ifexists("AssessedResourceId", "")),
AssessmentText = tostring(column_ifexists("RecommendationDisplayName", ""))
| where AssessmentResource =~ AppResourceId
| summarize PhpAssessmentRows = countif(AssessmentText contains "PHP"
or AssessmentText contains "CVE-2026-109618" or AssessmentText contains "CVE-2026-109619"),
ExposureAssessmentRows = countif(AssessmentText has_any ("public", "internet")),
AssessmentNamesToReview = make_set(AssessmentText)
| extend JoinKey = 1;
AppEvents
| join kind=inner Waf on RunId
| extend JoinKey = 1
| join kind=leftouter Posture on JoinKey
| where LastIngested > ago(15m)
| project TimeGenerated = LastSeen, RunId, AppResourceId,
FirstSeen, LastSeen, SyntheticSignals, WafEvents, WafActions, WafResourceIds,
Hostnames, ClientIps, Identities, PhpAssessmentRows, ExposureAssessmentRows,
AssessmentNamesToReview, EvidenceKind = "synthetic-training-correlation-not-compromise"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "lab-correlation",
"platform": "lab-workspace",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [],
"rows": null,
"queryTemplateSha256": "a003b61b37262b40c6d9eb142e35ea7600a3b7a83528f310f7510eaa58f737bb"
}
tvm-exposure
defender-xdr / detections/defender-xdr/tvm-exposure.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: Defender XDR TVM onboarding and published CVE detection required.
Required: DeviceTvmSoftwareVulnerabilities.
Reviewed query template
// Defender XDR: requires TVM coverage and published detection for these CVEs.
// Inventory snapshots have no event Timestamp. Empty results are not clearance.
DeviceTvmSoftwareVulnerabilities
| where CveId in~ ("CVE-2026-109618", "CVE-2026-109619")
| project DeviceId, DeviceName, OSPlatform, SoftwareName, SoftwareVersion,
CveId, RecommendedSecurityUpdate,
EvidenceKind = "reported-vulnerability-review-required"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "tvm-exposure",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareVulnerabilities"
],
"rows": null,
"queryTemplateSha256": "484987d63987667ce886c9da609944211f0dc917a832c852068ccb60117f2409"
}
php-inventory
defender-xdr / detections/defender-xdr/php-inventory.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: MDE software inventory required.
Required: DeviceTvmSoftwareInventory.
Reviewed query template
// Defender XDR: PHP-named inventory is a lead, not proof of a vulnerable runtime.
// Fixed versions: PHP advisories GHSA-w8hw-w9fx-j79w / GHSA-6wmf-4rv5-p3rp.
// Package revisions, epochs and prereleases require vendor/backport review.
let FixedBranches = datatable(Branch:string, FixedVersion:string)
[
"8.2", "8.2.35",
"8.3", "8.3.36",
"8.4", "8.4.27",
"8.5", "8.5.12"
];
DeviceTvmSoftwareInventory
| where SoftwareName contains "php"
| extend RuntimeRole = case(
SoftwareName matches regex @"(?i)(phpmyadmin|phpbb|phpunit|phpseclib|phpldapadmin)", "application-version-not-runtime",
SoftwareName matches regex @"(?i)(mod[-_]?php|libapache2-mod-php|libphp)", "apache-module-package",
SoftwareName contains "fpm", "fpm",
SoftwareName contains "cgi", "cgi",
SoftwareName matches regex @"(?i)(^|[-_])cli($|[-_])", "cli-not-web-facing-by-itself",
"unclassified-php-name")
| extend RuntimeVersion = extract(@"^(?:[0-9]+:)?([0-9]+\.[0-9]+\.[0-9]+)(?:$|[-+~.])", 1, SoftwareVersion),
Branch = extract(@"^(?:[0-9]+:)?([0-9]+\.[0-9]+)\.", 1, SoftwareVersion)
| extend VendorPackaged = isnotempty(RuntimeVersion) and SoftwareVersion != RuntimeVersion
| join kind=leftouter FixedBranches on Branch
| extend ReviewStatus = case(
RuntimeRole == "application-version-not-runtime", "check-serving-runtime-not-application-version",
isempty(RuntimeVersion) or isnull(parse_version(RuntimeVersion)), "unparsed-version-review-required",
VendorPackaged, "vendor-backport-or-prerelease-review-required",
parse_version(RuntimeVersion) < parse_version("8.2.0"), "unsupported-upstream-branch-check-vendor",
isempty(FixedVersion), "branch-outside-reference-review-required",
parse_version(RuntimeVersion) < parse_version(FixedVersion), "below-upstream-fixed-version",
"upstream-threshold-met-verify-deployment")
| project DeviceId, DeviceName, OSPlatform, SoftwareVendor, SoftwareName,
SoftwareVersion, RuntimeRole, RuntimeVersion, FixedVersion, VendorPackaged,
ReviewStatus, EvidenceKind = "inventory-version-lead-not-finding"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "php-inventory",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareInventory"
],
"rows": null,
"queryTemplateSha256": "15216f6dc1258c6f6a8fa2eb3c98da322ce8d037a40fbfa0d4dcf9a22c3ca2e0"
}
software-discovery
defender-xdr / detections/defender-xdr/software-discovery.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: inventory plus supported MDE process observations required.
Required: DeviceTvmSoftwareInventory, DeviceProcessEvents.
Reviewed query template
// Defender XDR: review distinct PHP names AND all software on observed web hosts.
// This discovers candidates outside a fixed product list; it does not prove PHP.
let WebHosts = DeviceProcessEvents
| where Timestamp between (ago(30d) .. now())
| where FileName matches regex @"(?i)^(php.*|lsphp.*|apache.*|httpd.*|nginx.*|w3wp\.exe|litespeed|lshttpd|frankenphp)$"
| distinct DeviceId;
DeviceTvmSoftwareInventory
| where SoftwareName contains "php" or DeviceId in (WebHosts)
| extend DiscoveryReason = iff(SoftwareName contains "php", "php-name", "software-on-observed-web-host")
| summarize Devices = dcount(DeviceId), Versions = make_set(SoftwareVersion, 20),
SampleDevices = make_set(DeviceName, 10) by SoftwareVendor, SoftwareName, DiscoveryReason
| project SoftwareVendor, SoftwareName, DiscoveryReason, Devices, Versions,
SampleDevices, EvidenceKind = "software-discovery-lead-review-required"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "software-discovery",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareInventory",
"DeviceProcessEvents"
],
"rows": null,
"queryTemplateSha256": "ac1c9fe1fe793638f98004b919e2b86cc51672f717683be224fc1951aa287807"
}
php-binary-evidence
defender-xdr / detections/defender-xdr/php-binary-evidence.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: recognized TVM software evidence; beta schema needs live verification.
Required: DeviceTvmSoftwareEvidenceBeta, DeviceInfo.
Reviewed query template
// Defender XDR: beta TVM evidence schema may change; only recognized software appears.
// Disk evidence is not an exhaustive filesystem scan or proof a module is loaded.
DeviceTvmSoftwareEvidenceBeta
| mv-expand DiskPath = DiskPaths to typeof(string)
| where DiskPath matches regex @"(?i)([/\\]php([0-9.]*|[0-9.]*-?fpm|[0-9.]*-cgi)(\.exe)?$|libphp[0-9.]*\.so|php[0-9]*(ts|apache2[_0-9]*)?\.dll)"
| join kind=leftouter (
DeviceInfo
| where Timestamp between (ago(30d) .. now())
| summarize arg_max(Timestamp, DeviceName, OSPlatform) by DeviceId
) on DeviceId
| project DeviceId, DeviceName, OSPlatform, SoftwareVendor, SoftwareName,
SoftwareVersion, DiskPath, LastSeenTime,
EvidenceKind = "recognized-software-disk-evidence-not-loaded-proof"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "php-binary-evidence",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareEvidenceBeta",
"DeviceInfo"
],
"rows": null,
"queryTemplateSha256": "4d75f53d3b01953276ee6b4b81a59641f4c5075bed342a5d5ca0556d6d51af40"
}
bundled-products
defender-xdr / detections/defender-xdr/bundled-products.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: TVM inventory and explicitly reviewed product input required.
Required: DeviceTvmSoftwareInventory.
Reviewed query template
// Defender XDR: populate reviewed NCSC/vendor product rows with prepare-products.mjs.
// Empty input returns no leads, NOT a completed product review. No remote feed runs.
// Scope inventory to approved devices before a large keyword join; names are leads.
let ReviewedProducts = datatable(Keyword:string, Product:string, Status109618:string, Status109619:string, Source:string)
[
// REVIEWED_PRODUCT_ROWS
];
ReviewedProducts
| where isnotempty(Keyword)
| extend MatchKey = 1
| join kind=inner (DeviceTvmSoftwareInventory | extend MatchKey = 1) on MatchKey
| where tolower(SoftwareName) contains tolower(Keyword)
| project DeviceId, DeviceName, SoftwareVendor, SoftwareName, SoftwareVersion,
Keyword, Product, Status109618, Status109619, Source,
EvidenceKind = "product-name-lead-check-serving-runtime"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "bundled-products",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareInventory"
],
"rows": null,
"queryTemplateSha256": "9539de8c8008491acd119ef3ebb95af642efb4610c769abbba4c571e0133bca2"
}
worker-classification
defender-xdr / detections/defender-xdr/worker-classification.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: supported MDE telemetry and inventory required.
Required: DeviceTvmSoftwareInventory, DeviceProcessEvents.
Reviewed query template
// Defender XDR: process START observations do not establish current running state.
// Apache plus a mod_php package is a prioritization proxy, not loaded-module proof.
// Include installed-but-quiet devices, even when no process started in the window.
let ModulePackages = DeviceTvmSoftwareInventory
| where SoftwareName matches regex @"(?i)(mod[-_]?php|libapache2-mod-php|libphp)"
| summarize ModPhpPackages = make_set(strcat(SoftwareName, " ", SoftwareVersion), 30) by DeviceId, DeviceName;
let Starts = DeviceProcessEvents
| where Timestamp between (ago(30d) .. now())
| where FileName matches regex @"(?i)^(apache2?(\.exe)?|httpd(\.exe)?|php[-0-9.]*fpm[0-9.]*|php-cgi(\.exe)?|lsphp[0-9.]*|frankenphp)$"
| summarize ApacheStarts = countif(FileName matches regex @"(?i)^(apache2?(\.exe)?|httpd(\.exe)?)$"),
WorkerStarts = countif(FileName matches regex @"(?i)^(php[-0-9.]*fpm[0-9.]*|php-cgi(\.exe)?|lsphp[0-9.]*|frankenphp)$"),
WorkerBinaries = make_set(FileName, 30), LastProcessStart = max(Timestamp) by DeviceId, DeviceName;
union (ModulePackages | project DeviceId, DeviceName), (Starts | project DeviceId, DeviceName)
| summarize DeviceName = take_any(DeviceName) by DeviceId
| join kind=leftouter ModulePackages on DeviceId
| join kind=leftouter Starts on DeviceId
| extend Priority = case(
array_length(ModPhpPackages) > 0 and ApacheStarts > 0, 1,
WorkerStarts > 0, 2,
array_length(ModPhpPackages) > 0, 3,
4)
| extend Classification = case(
Priority == 1, "apache-plus-module-package-proxy",
Priority == 2, "php-worker-start-observed",
Priority == 3, "module-package-installed-no-recent-start",
"apache-start-no-module-package-evidence")
| project DeviceId, DeviceName, Priority, Classification, ModPhpPackages,
WorkerBinaries, LastProcessStart,
EvidenceKind = "runtime-start-observation-not-live-state"
| order by Priority asc, DeviceName asc
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "worker-classification",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceTvmSoftwareInventory",
"DeviceProcessEvents"
],
"rows": null,
"queryTemplateSha256": "4b681cd56419ea9c114262b6b0a76295c32a827800360473f9cce2b6f617382b"
}
worker-discovery
defender-xdr / detections/defender-xdr/worker-discovery.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: process and OS-dependent inbound-network telemetry required.
Required: DeviceProcessEvents, DeviceNetworkEvents.
Reviewed query template
// Defender XDR: discover unfamiliar launchers and inbound web listeners.
// Inbound telemetry varies by OS. Neither branch is an exhaustive worker list.
let Starts = DeviceProcessEvents
| where Timestamp between (ago(30d) .. now())
| where FileName contains "php" or ProcessCommandLine contains "php"
| summarize Events = count(), Devices = dcount(DeviceId), LastSeen = max(Timestamp),
SampleDevices = make_set(DeviceName, 10) by Process = FileName
| extend DiscoveryKind = "php-name-or-command";
let Listeners = DeviceNetworkEvents
| where Timestamp between (ago(30d) .. now())
| where ActionType == "InboundConnectionAccepted" and LocalPort in (80, 443, 8000, 8080, 8443, 8888)
| summarize Events = count(), Devices = dcount(DeviceId), LastSeen = max(Timestamp),
SampleDevices = make_set(DeviceName, 10) by Process = InitiatingProcessFileName
| extend DiscoveryKind = "inbound-web-port";
union Starts, Listeners
| project DiscoveryKind, Process, Events, Devices, LastSeen, SampleDevices,
EvidenceKind = "worker-discovery-lead-review-required"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "worker-discovery",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceProcessEvents",
"DeviceNetworkEvents"
],
"rows": null,
"queryTemplateSha256": "e72d9e2262c0fa0937f16cf125b905fa8226b93363f8b6b60e2f96f19f1b41ea"
}
worker-baseline
defender-xdr / detections/defender-xdr/worker-baseline.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: supported MDE process telemetry required.
Required: DeviceProcessEvents.
Reviewed query template
// Defender XDR: baseline legitimate children before tuning the worker hunts.
// Repeated patterns are not automatically benign. Review app owners and changes.
let WorkerPattern = @"(?i)^(php(?:[0-9.]*-?fpm[0-9.]*|-cgi|[0-9.]*)?(?:\.exe)?|lsphp[0-9.]*|apache2?(?:\.exe)?|httpd(?:\.exe)?|w3wp\.exe|nginx|litespeed|lshttpd|frankenphp)$";
DeviceProcessEvents
| where Timestamp between (ago(30d) .. now())
| where InitiatingProcessFileName matches regex WorkerPattern
| summarize Executions = count(), Devices = dcount(DeviceId), LastSeen = max(Timestamp)
by ParentWorker = InitiatingProcessFileName, ChildProcess = FileName
| project ParentWorker, ChildProcess, Executions, Devices, LastSeen,
EvidenceKind = "observed-worker-child-baseline"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "worker-baseline",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceProcessEvents"
],
"rows": null,
"queryTemplateSha256": "c48f71d4a0e9184cec0999120a51fee74e32bec728609e8f2604cc70f1527ea0"
}
worker-child-processes
defender-xdr / detections/defender-xdr/worker-child-processes.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: real MDE child-process events required; never generated by the simulator.
Required: DeviceProcessEvents.
Reviewed query template
// Defender XDR: requires supported MDE process telemetry; not emitted by this lab.
// Legitimate composer, image, mail and deployment activity needs contextual review.
// In-worker execution has no child process; no match is not proof of safety.
let WorkerPattern = @"(?i)^(php(?:[0-9.]*-?fpm[0-9.]*|-cgi|[0-9.]*)?(?:\.exe)?|lsphp[0-9.]*|apache2?(?:\.exe)?|httpd(?:\.exe)?|w3wp\.exe|nginx|litespeed|lshttpd|frankenphp)$";
DeviceProcessEvents
| where Timestamp between (ago(30d) .. now())
| where InitiatingProcessFileName matches regex WorkerPattern
| extend Child = tolower(FileName)
| extend Category = case(
Child in ("sh", "bash", "dash", "ash", "zsh", "ksh", "csh", "cmd.exe", "powershell.exe", "pwsh", "pwsh.exe"), "shell",
Child matches regex @"^(python[0-9.]*|perl|ruby|node|lua)(\.exe)?$", "interpreter",
Child in ("curl", "wget", "nc", "ncat", "netcat", "socat", "busybox", "tftp", "certutil.exe", "bitsadmin.exe", "mshta.exe", "rundll32.exe", "regsvr32.exe"), "network-or-system-tool",
Child in ("whoami", "id", "uname", "hostname", "ifconfig", "ipconfig", "ip", "netstat", "ss", "ps", "env", "cat", "ls", "find", "net.exe", "net1.exe", "systeminfo", "tasklist", "nltest.exe"), "discovery-tool",
"")
| where isnotempty(Category)
| extend SuspiciousCommandShape = ProcessCommandLine matches regex @"(?i)(/dev/(tcp|udp)/|mkfifo|encodedcommand|downloadstring|fsockopen)"
| extend Priority = iff(SuspiciousCommandShape, 1, 2)
| project Timestamp, DeviceId, DeviceName, ReportId, Priority, Category,
ParentWorker = InitiatingProcessFileName, ChildProcess = FileName,
ProcessCommandLine, AccountName, SuspiciousCommandShape,
EvidenceKind = "observed-worker-child-review-required"
| order by Priority asc, Timestamp desc
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "worker-child-processes",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceProcessEvents"
],
"rows": null,
"queryTemplateSha256": "daa822601cf17c62127d6e0d0bff6c78f458884f8afba479615bbe5bdade2890"
}
php-file-drops
defender-xdr / detections/defender-xdr/php-file-drops.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: real MDE file events required; no files are dropped by the simulator.
Required: DeviceFileEvents.
Reviewed query template
// Defender XDR: requires MDE file telemetry; CMS/plugin/cache writes cause noise.
// A path-based create/delete association is a lead, not proof of file execution.
let WorkerPattern = @"(?i)^(php(?:[0-9.]*-?fpm[0-9.]*|-cgi|[0-9.]*)?(?:\.exe)?|lsphp[0-9.]*|apache2?(?:\.exe)?|httpd(?:\.exe)?|w3wp\.exe|nginx|litespeed|lshttpd|frankenphp)$";
let Writes = DeviceFileEvents
| where Timestamp between (ago(30d) .. now())
| where ActionType in ("FileCreated", "FileModified", "FileRenamed")
| where FileName matches regex @"(?i)\.(php[3-8]?|phtml|pht|phar|inc)$"
| where InitiatingProcessFileName matches regex WorkerPattern
or FolderPath matches regex @"(?i)(^/(tmp|var/tmp|dev/shm)/|\\temp\\)"
| project CreatedAt = Timestamp, DeviceId, DeviceName, ReportId, FolderPath,
FileName, Writer = InitiatingProcessFileName, SHA1;
let Deletes = DeviceFileEvents
| where Timestamp between (ago(30d) .. now()) and ActionType == "FileDeleted"
| project DeletedAt = Timestamp, DeviceId, FolderPath, FileName;
Writes
| join kind=leftouter Deletes on DeviceId, FolderPath, FileName
| summarize RapidDelete = max(toint(isnotnull(DeletedAt) and DeletedAt between (CreatedAt .. (CreatedAt + 10m))))
by Timestamp = CreatedAt, DeviceId, DeviceName, ReportId, FolderPath, FileName, Writer, SHA1
| project Timestamp, DeviceId, DeviceName, ReportId, FolderPath, FileName, Writer,
SHA1, DeletedWithin10Min = RapidDelete == 1,
EvidenceKind = "observed-php-file-lifecycle-review-required"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "php-file-drops",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceFileEvents"
],
"rows": null,
"queryTemplateSha256": "62ca0964c79b5d521b49809dba70252254844f171efd28f6591bdf0492f3b876"
}
worker-network
defender-xdr / detections/defender-xdr/worker-network.kql
Without optional protection: Web/WAF logs do not reconstruct device software, process ancestry, disk writes or endpoint network activity.
Additional value: Onboarded, supported MDE/TVM sources add those device-level observations and published CVE findings. App Service is not an MDE device; this console has no tenant-wide XDR hunting consent.
Telemetry: Not provided: real MDE network events required; no egress is simulated.
Required: DeviceNetworkEvents.
Reviewed query template
// Defender XDR: requires MDE network telemetry; no connections are simulated here.
// A worker's child tool is retained even on 80/443. Direct worker API/mail traffic
// on common ports is excluded; review that blind spot and private destinations too.
let WorkerPattern = @"(?i)^(php(?:[0-9.]*-?fpm[0-9.]*|-cgi|[0-9.]*)?(?:\.exe)?|lsphp[0-9.]*|apache2?(?:\.exe)?|httpd(?:\.exe)?|w3wp\.exe|nginx|litespeed|lshttpd|frankenphp)$";
DeviceNetworkEvents
| where Timestamp between (ago(30d) .. now())
| where ActionType in ("ConnectionSuccess", "ConnectionAttempt", "ConnectionFailed")
| where RemoteIPType =~ "Public"
| extend WorkerDirect = InitiatingProcessFileName matches regex WorkerPattern and RemotePort !in (80, 443, 53, 25, 465, 587),
WorkerChild = InitiatingProcessParentFileName matches regex WorkerPattern
and InitiatingProcessFileName matches regex @"(?i)^(sh|bash|dash|ash|zsh|ksh|python[0-9.]*|perl|ruby|node|curl|wget|nc|ncat|netcat|socat|cmd|powershell|pwsh|certutil|bitsadmin)(\.exe)?$"
| where WorkerDirect or WorkerChild
| project Timestamp, DeviceId, DeviceName, ReportId,
Signal = iff(WorkerChild, "worker-child-tool-public-connection", "worker-unusual-public-port"),
Process = InitiatingProcessFileName, ParentWorker = InitiatingProcessParentFileName,
RemoteIP, RemotePort, RemoteUrl, ActionType,
EvidenceKind = "observed-network-lead-review-required"
Actual result: unavailable / 2026-10-24T18:35:02.072Z; not executed.
No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.
Reviewed result and public fields
{
"id": "worker-network",
"platform": "defender-xdr",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": null,
"reason": "No XDR hunting connector/tenant-wide consent is configured. App Service does not supply MDE device telemetry.",
"missingTables": [
"DeviceNetworkEvents"
],
"rows": null,
"queryTemplateSha256": "32a08d4944c560f039aed488c3ed1698cf519a9ae8b1c6f936eb89a3a6adaa92"
}
php-assessments
resource-graph / detections/resource-graph/php-assessments.kql
Without optional protection: ARM runtime configuration and foundational recommendations identify leads; major.minor stack strings are not patch evidence.
Additional value: Applicable Defender assessments and paid CSPM graph/attack-path context can prioritize actual affected assets. Empty findings and text matches never prove coverage or safety.
Telemetry: Optional actual posture assessments; discovery and coverage are not guaranteed.
Required: securityresources.
Reviewed query template
// Resource Graph: set an explicitly authorized subscription/resource-group scope.
// Requires actual Defender assessments; text matches are not confirmed findings.
securityresources
| where type in~ ("microsoft.security/assessments", "microsoft.security/assessments/subassessments")
| extend Details = tostring(properties)
| where Details contains "PHP" or Details contains "CVE-2026-109618" or Details contains "CVE-2026-109619"
| project AssessmentId = id, subscriptionId, resourceGroup,
ResourceId = coalesce(tostring(properties.resourceDetails.id), tostring(properties.resourceDetails.Id)),
DisplayName = tostring(properties.displayName), Status = tostring(properties.status.code),
AssessmentTime = todatetime(properties.timeGenerated),
EvidenceKind = "assessment-text-match-review-required"
Actual result: queried / 2026-10-24T18:35:02.072Z; 4 rows.
Reviewed result and public fields
{
"id": "php-assessments",
"platform": "resource-graph",
"status": "queried",
"collectedAt": "2026-10-24T18:35:02.072Z",
"rowCount": 4,
"rows": [
{
"Status": "Healthy",
"EvidenceKind": "assessment-text-match-review-required"
},
{
"Status": "Healthy",
"EvidenceKind": "assessment-text-match-review-required"
},
{
"Status": "Healthy",
"EvidenceKind": "assessment-text-match-review-required"
},
{
"Status": "Healthy",
"EvidenceKind": "assessment-text-match-review-required"
}
],
"queryTemplateSha256": "f88a8d5b52084c94ebc858e396d24f27ab7e74a3dcaea7d3b0a2cf9232de9fb9"
}
appservice-runtimes
resource-graph / detections/resource-graph/appservice-runtimes.kql
Without optional protection: ARM runtime configuration and foundational recommendations identify leads; major.minor stack strings are not patch evidence.
Additional value: Applicable Defender assessments and paid CSPM graph/attack-path context can prioritize actual affected assets. Empty findings and text matches never prove coverage or safety.
Telemetry: Lab resource configuration is available; effective patch version is not established by Resource Graph.
Required: resources.
Reviewed query template
// Resource Graph: configuration reports a stack, NOT its effective patch level.
// Unknown config and custom containers remain review candidates, including slots.
// For empty config use the authorized app's read-only webapp config show fallback.
resources
| where type in~ ("microsoft.web/sites", "microsoft.web/sites/slots")
| extend LinuxStack = tostring(properties.siteConfig.linuxFxVersion),
WindowsPhp = tostring(properties.siteConfig.phpVersion)
| extend CandidateKind = case(
LinuxStack startswith "PHP|", "php-platform-stack",
LinuxStack startswith "WORDPRESS|", "wordpress-platform-stack",
LinuxStack startswith "DOCKER|" or LinuxStack startswith "COMPOSE|" or LinuxStack startswith "KUBE|", "custom-container-inspect-image",
WindowsPhp !in~ ("", "off", "0"), "windows-php-config",
isempty(LinuxStack), "runtime-not-reported-review-config",
"other-reported-runtime")
| where CandidateKind != "other-reported-runtime"
| project ResourceId = id, AppName = name, subscriptionId, resourceGroup, location,
Runtime = coalesce(LinuxStack, WindowsPhp), CandidateKind,
DefaultHostName = tostring(properties.defaultHostName),
ReviewStatus = "verify-effective-runtime-and-patch-level",
EvidenceKind = "runtime-configuration-lead-not-patch-proof"
Actual result: queried / 2026-10-24T18:35:02.475Z; 1 rows.
Reviewed result and public fields
{
"id": "appservice-runtimes",
"platform": "resource-graph",
"status": "queried",
"collectedAt": "2026-10-24T18:35:02.475Z",
"rowCount": 1,
"rows": [
{
"Runtime": "PHP|8.4",
"CandidateKind": "php-platform-stack",
"ReviewStatus": "verify-effective-runtime-and-patch-level",
"EvidenceKind": "runtime-configuration-lead-not-patch-proof"
}
],
"queryTemplateSha256": "e4b193eb85a7b811cd00b0bccc1c265cc582968a48e95ad3b83de875ac12feb5"
}
appservice-runtime-discovery
resource-graph / detections/resource-graph/appservice-runtime-discovery.kql
Without optional protection: ARM runtime configuration and foundational recommendations identify leads; major.minor stack strings are not patch evidence.
Additional value: Applicable Defender assessments and paid CSPM graph/attack-path context can prioritize actual affected assets. Empty findings and text matches never prove coverage or safety.
Telemetry: Read-only inventory within the operator's approved Resource Graph scope.
Required: resources.
Reviewed query template
// Resource Graph: review ALL runtime strings, not only a fixed PHP stack list.
// An unreported runtime is a collection/configuration gap, not proof of no PHP.
resources
| where type in~ ("microsoft.web/sites", "microsoft.web/sites/slots")
| extend LinuxStack = tostring(properties.siteConfig.linuxFxVersion),
WindowsPhp = tostring(properties.siteConfig.phpVersion)
| extend Runtime = case(
isnotempty(LinuxStack), LinuxStack,
WindowsPhp !in~ ("", "off", "0"), strcat("Windows PHP ", WindowsPhp),
"(runtime not reported)")
| summarize Apps = count(), SampleResourceIds = make_set(id, 10) by Runtime
| project Runtime, Apps, SampleResourceIds,
EvidenceKind = "runtime-discovery-review-required"
Actual result: queried / 2026-10-24T18:35:02.944Z; 1 rows.
Reviewed result and public fields
{
"id": "appservice-runtime-discovery",
"platform": "resource-graph",
"status": "queried",
"collectedAt": "2026-10-24T18:35:02.944Z",
"rowCount": 1,
"rows": [
{
"Runtime": "PHP|8.4",
"Apps": 1,
"EvidenceKind": "runtime-discovery-review-required"
}
],
"queryTemplateSha256": "1bd74a23eec6081e2a88617e9ac303a7051e9cb3447ab28821296350fca5aa49"
}
container-cve-findings
resource-graph / detections/resource-graph/container-cve-findings.kql
Without optional protection: ARM runtime configuration and foundational recommendations identify leads; major.minor stack strings are not patch evidence.
Additional value: Applicable Defender assessments and paid CSPM graph/attack-path context can prioritize actual affected assets. Empty findings and text matches never prove coverage or safety.
Telemetry: Not provided: registry/container assessment coverage and CVE logic required.
Required: securityresources.
Reviewed query template
// Resource Graph: only ACR-scoped image assessments, not arbitrary VM findings.
// Requires assessment coverage and published CVE logic; zero rows is not clearance.
// Supports legacy subassessments and newer securityentitydata/CvesDetails rows.
securityresources
| where type in~ ("microsoft.security/assessments", "microsoft.security/assessments/subassessments")
| extend Details = tostring(properties), Additional = properties.additionalData
| where Details contains "CVE-2026-109618" or Details contains "CVE-2026-109619"
| extend RegistryId = coalesce(
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, id),
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, tostring(properties.resourceDetails.id)),
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, tostring(properties.resourceDetails.Id)))
| where isnotempty(RegistryId)
| extend CveRows = iff(type =~ "microsoft.security/assessments/subassessments",
dynamic([{}]), parse_json(tostring(Additional.CvesDetails)))
| mv-expand Cve = CveRows
| where type =~ "microsoft.security/assessments/subassessments"
or tostring(Cve.CveId) in~ ("CVE-2026-109618", "CVE-2026-109619")
| project AssessmentId = id, RegistryId, subscriptionId, resourceGroup,
Repository = coalesce(tostring(Additional.artifactDetails.repositoryName), tostring(Additional.repositoryName)),
ImageDigest = coalesce(tostring(Additional.artifactDetails.digest), tostring(Additional.imageDigest),
extract(@"(?i)/securityentitydata/[^/]+-images-(sha256:[a-f0-9]{64})(?:/|$)", 1, id)),
VulnerabilityId = coalesce(tostring(Cve.CveId), tostring(properties.id)), DisplayName = tostring(properties.displayName),
Status = tostring(properties.status.code),
Severity = coalesce(tostring(Cve.Severity), tostring(properties.status.severity), tostring(properties.metadata.severity)),
EvidenceKind = "reported-container-cve-review-required"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "container-cve-findings",
"platform": "resource-graph",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"securityresources"
],
"rows": null,
"queryTemplateSha256": "01b852531f4792fd91da39f01d22cc3606bbea194b00ea57afe9e97fa675673a"
}
container-php-findings
resource-graph / detections/resource-graph/container-php-findings.kql
Without optional protection: ARM runtime configuration and foundational recommendations identify leads; major.minor stack strings are not patch evidence.
Additional value: Applicable Defender assessments and paid CSPM graph/attack-path context can prioritize actual affected assets. Empty findings and text matches never prove coverage or safety.
Telemetry: Not provided: registry findings required; zero-finding images need independent SBOM inventory.
Required: securityresources.
Reviewed query template
// Resource Graph: PHP-related finding text is a lead, not a full image inventory.
// Images with zero findings require an authorized digest-based SBOM review.
// Newer image findings can be primary assessments, not just subassessments.
securityresources
| where type in~ ("microsoft.security/assessments", "microsoft.security/assessments/subassessments")
| extend Additional = properties.additionalData, Details = tostring(properties)
| where Details matches regex @"(?i)(\bphp[0-9.]*\b|libphp|mod_php|libapache2-mod-php)"
| extend RegistryId = coalesce(
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, id),
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, tostring(properties.resourceDetails.id)),
extract(@"(?i)^(.*/providers/Microsoft\.ContainerRegistry/registries/[^/]+)", 1, tostring(properties.resourceDetails.Id)))
| where isnotempty(RegistryId)
| extend Repository = coalesce(tostring(Additional.artifactDetails.repositoryName), tostring(Additional.repositoryName)),
ImageDigest = coalesce(tostring(Additional.artifactDetails.digest), tostring(Additional.imageDigest),
extract(@"(?i)/securityentitydata/[^/]+-images-(sha256:[a-f0-9]{64})(?:/|$)", 1, id))
| summarize FindingRows = count(), SampleAssessmentIds = make_set(id, 10)
by RegistryId, Repository, ImageDigest, subscriptionId, resourceGroup
| project RegistryId, Repository, ImageDigest, subscriptionId, resourceGroup,
FindingRows, SampleAssessmentIds,
EvidenceKind = "php-finding-text-lead-not-image-inventory"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "container-php-findings",
"platform": "resource-graph",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"securityresources"
],
"rows": null,
"queryTemplateSha256": "9997615f28960f8486f41489eff42428a20891c09b61a6f1a7fa4eaa083204f3"
}
syslog-worker-failures
sentinel-log-analytics / detections/sentinel-log-analytics/syslog-worker-failures.kql
Without optional protection: Collected source logs can be queried manually without enabling Microsoft Sentinel.
Additional value: Explicitly configured Sentinel analytics can correlate ingested sources. Missing DCRs/tables, disabled rules and late ingestion remain gaps; a query result is not an alert or incident.
Telemetry: Not provided: supported Linux host, AMA and a Syslog DCR required.
Required: Syslog.
Reviewed query template
// Log Analytics: requires AMA + Syslog DCR on supported Linux web hosts.
// App error files only reach Syslog if explicitly routed there. Fixed 10m bins
// can split bursts. Crashes, OOM and churn are leads, not exploitation proof.
Syslog
| where TimeGenerated between (ago(14d) .. now())
| where SyslogMessage matches regex @"(?i)(php|libphp|apache|httpd|lsphp)"
or ProcessName matches regex @"(?i)(php|apache|httpd|lsphp)"
| extend Signal = case(
SyslogMessage contains "segfault", "worker-segfault",
SyslogMessage contains "out of memory" or SyslogMessage contains "oom-kill", "worker-oom",
SyslogMessage contains "Allowed memory size", "php-memory-limit",
SyslogMessage contains "Maximum execution time", "php-execution-time",
SyslogMessage matches regex @"(?i)child( pid)? [0-9]+ exit(ed)? (on |with |)signal", "worker-signal-exit",
SyslogMessage matches regex @"(?i)child [0-9]+ exited with code [0-9]+ after", "worker-churn",
"")
| where isnotempty(Signal)
| summarize Events = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
by TimeGenerated = bin(TimeGenerated, 10m), ResourceId = _ResourceId, Computer, Signal
| project TimeGenerated, ResourceId, Computer, Signal, Events, FirstSeen, LastSeen,
IsBurst = Events >= 5, EvidenceKind = "observed-worker-failure-review-required"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "syslog-worker-failures",
"platform": "sentinel-log-analytics",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"Syslog"
],
"rows": null,
"queryTemplateSha256": "661f8bd52f6e03741e8ef625d75b51b543b36855a08a9a1ee976e8af6aebe5de"
}
appservice-http
sentinel-log-analytics / detections/sentinel-log-analytics/appservice-http.kql
Without optional protection: Collected source logs can be queried manually without enabling Microsoft Sentinel.
Additional value: Explicitly configured Sentinel analytics can correlate ingested sources. Missing DCRs/tables, disabled rules and late ingestion remain gaps; a query result is not an alert or incident.
Telemetry: Real HTTP diagnostics after ingestion; harmless GET simulation is not an expected hunt hit.
Required: AppServiceHTTPLogs.
Reviewed query template
// Log Analytics: requires AppServiceHTTPLogs diagnostics, enabled by this lab.
// No Content-Type, request body or response cookie is available in this table.
// Request cookies are measured, never projected. Short bins are clusters, not
// proof of an ordered exploit sequence; custom session names need separate review.
let Requests = AppServiceHTTPLogs
| where TimeGenerated between (ago(14d) .. now())
| project TimeGenerated, ResourceId = tolower(_ResourceId), Hostname = tolower(CsHost),
ClientIp = CIp, Method = CsMethod, Status = ScStatus, RequestBytes = CsBytes,
RequestCookieLength = strlen(Cookie),
HasSession = Cookie matches regex @"(?i)(^|;\s*)PHPSESSID=", Uri = CsUriStem;
let Bursts = Requests
| where Method == "POST"
| summarize Events = count(), Errors = countif(Status between (500 .. 599)),
MaxRequestBytes = max(RequestBytes), MaxRequestCookieLength = max(RequestCookieLength),
SessionPosts = countif(HasSession), Uris = make_set(Uri, 5)
by TimeGenerated = bin(TimeGenerated, 5m), ResourceId, Hostname, ClientIp
| where Errors >= 5 and MaxRequestBytes > 100000
| extend Signal = "large-post-server-error-burst";
let SessionClusters = Requests
| where Method == "POST"
| summarize Events = count(), Errors = countif(Status between (500 .. 599)),
MaxRequestBytes = max(RequestBytes), MaxRequestCookieLength = max(RequestCookieLength),
SessionPosts = countif(HasSession), Uris = make_set(Uri, 5)
by TimeGenerated = bin(TimeGenerated, 10s), ResourceId, Hostname, ClientIp
| where Events >= 2 and SessionPosts >= 1 and Errors >= 1
| extend Signal = "short-post-session-error-cluster";
let LargeCookies = Requests
| where RequestCookieLength > 1000
| summarize Events = count(), Errors = countif(Status between (500 .. 599)),
MaxRequestBytes = max(RequestBytes), MaxRequestCookieLength = max(RequestCookieLength),
SessionPosts = countif(Method == "POST" and HasSession), Uris = make_set(Uri, 5)
by TimeGenerated = bin(TimeGenerated, 5m), ResourceId, Hostname, ClientIp
| extend Signal = "large-request-cookie";
union Bursts, SessionClusters, LargeCookies
| project TimeGenerated, ResourceId, Hostname, ClientIp, Signal, Events, Errors,
MaxRequestBytes, MaxRequestCookieLength, SessionPosts, Uris,
EvidenceKind = "observed-http-heuristic-not-exploit-proof"
Actual result: unavailable / 2026-10-24T18:35:03.365Z; not executed.
Azure evidence exceeds the 1 MiB response bound.
Reviewed result and public fields
{
"id": "appservice-http",
"platform": "sentinel-log-analytics",
"status": "unavailable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "Azure evidence exceeds the 1 MiB response bound.",
"missingTables": [
"AppServiceHTTPLogs"
],
"rows": null,
"queryTemplateSha256": "6a79eabab5747bcce49875d1f86e42e3d9276b5e026a9fece5bb8f29a737b326"
}
appgateway-waf
sentinel-log-analytics / detections/sentinel-log-analytics/appgateway-waf.kql
Without optional protection: Collected source logs can be queried manually without enabling Microsoft Sentinel.
Additional value: Explicitly configured Sentinel analytics can correlate ingested sources. Missing DCRs/tables, disabled rules and late ingestion remain gaps; a query result is not an alert or incident.
Telemetry: Not provided: this lab uses Front Door, not Application Gateway.
Required: AGWAccessLogs, AGWFirewallLogs.
Reviewed query template
// Log Analytics: requires resource-specific AGWAccessLogs + AGWFirewallLogs.
// This lab deploys Front Door, NOT Application Gateway. These are optional hunts.
// Rules 200002/200003 are generic parsing leads; verify the active ruleset.
// Correlate nonempty transaction IDs within the SAME gateway to avoid false joins.
let Access = AGWAccessLogs
| where TimeGenerated between (ago(14d) .. now())
| project TimeGenerated, ResourceId = tolower(_ResourceId), TransactionId,
AccessHost = Host, ClientIp, HttpMethod, ServerStatus, ReceivedBytes;
let Bursts = Access
| where HttpMethod == "POST"
| summarize Events = count(), BackendErrors = countif(ServerStatus between (500 .. 599)),
MaxRequestBytes = max(ReceivedBytes)
by TimeGenerated = bin(TimeGenerated, 5m), ResourceId, Hostname = AccessHost, ClientIp
| where BackendErrors >= 5 and MaxRequestBytes > 100000
| extend Signal = "gateway-large-post-backend-error-burst",
RuleId = "", WafAction = "", TransactionId = "";
let BodySignals = AGWFirewallLogs
| where TimeGenerated between (ago(14d) .. now())
| where RuleId in ("200002", "200003")
or Message contains "multipart" or DetailedMessage contains "multipart"
or Message contains "request body" or DetailedMessage contains "request body"
| project TimeGenerated, ResourceId = tolower(_ResourceId), TransactionId,
Hostname, ClientIp, RuleId, WafAction = Action
| join kind=leftouter (
Access
| where isnotempty(TransactionId)
| summarize arg_max(TimeGenerated, AccessHost, ServerStatus, ReceivedBytes) by ResourceId, TransactionId
) on ResourceId, TransactionId
| extend Hostname = coalesce(AccessHost, Hostname), Signal = "gateway-body-parsing-rule-lead",
Events = long(1), BackendErrors = iff(isnull(ServerStatus), long(null), tolong(iff(ServerStatus between (500 .. 599), 1, 0))),
MaxRequestBytes = ReceivedBytes;
union Bursts, BodySignals
| project TimeGenerated, ResourceId, Hostname, ClientIp, Signal, Events,
BackendErrors, MaxRequestBytes, RuleId, WafAction, TransactionId,
EvidenceKind = "observed-generic-waf-http-lead"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "appgateway-waf",
"platform": "sentinel-log-analytics",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"AGWAccessLogs",
"AGWFirewallLogs"
],
"rows": null,
"queryTemplateSha256": "c65602666540bb47bfae878d53ff08df9af6d52c5c8572fea7f0b87f0ef5d057"
}
container-worker-errors
sentinel-log-analytics / detections/sentinel-log-analytics/container-worker-errors.kql
Without optional protection: Collected source logs can be queried manually without enabling Microsoft Sentinel.
Additional value: Explicitly configured Sentinel analytics can correlate ingested sources. Missing DCRs/tables, disabled rules and late ingestion remain gaps; a query result is not an alert or incident.
Telemetry: Not provided: AKS Container Insights and actual stderr/stdout logging required.
Required: ContainerLogV2.
Reviewed query template
// Log Analytics: requires Container Insights ContainerLogV2, not deployed here.
// PHP/FPM must actually send errors/worker output to stdout/stderr for collection.
// LogMessage can be dynamic. Workload failures do not establish exploitation.
ContainerLogV2
| where TimeGenerated between (ago(14d) .. now())
| extend Message = tostring(LogMessage)
| extend Signal = case(
Message contains "Allowed memory size" or Message contains "Out of memory", "php-memory-error",
Message contains "Maximum execution time", "php-execution-time",
Message matches regex @"(?i)child( pid)? [0-9]+ exit(ed)? (on |with |)signal", "worker-signal-exit",
Message matches regex @"(?i)child [0-9]+ exited with code [0-9]+ after", "worker-churn",
Message matches regex @"(?i)PHP (Fatal error|Warning|Deprecated).*(session|upload_progress)", "php-session-diagnostic",
"")
| where isnotempty(Signal)
| summarize Events = count(), Containers = make_set(ContainerName, 10),
FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated)
by TimeGenerated = bin(TimeGenerated, 10m), ResourceId = _ResourceId, PodNamespace, PodName, Signal
| project TimeGenerated, ResourceId, PodNamespace, PodName, Signal, Events,
Containers, FirstSeen, LastSeen,
EvidenceKind = "observed-container-worker-error-review-required"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "container-worker-errors",
"platform": "sentinel-log-analytics",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"ContainerLogV2"
],
"rows": null,
"queryTemplateSha256": "76f5d8b8c9317185b755edaf9c1707dfa71812de6e494042b84e572773d8d224"
}
web-text-logs
sentinel-log-analytics / detections/sentinel-log-analytics/web-text-logs.kql
Without optional protection: Collected source logs can be queried manually without enabling Microsoft Sentinel.
Additional value: Explicitly configured Sentinel analytics can correlate ingested sources. Missing DCRs/tables, disabled rules and late ingestion remain gaps; a query result is not an alert or incident.
Telemetry: Not provided: custom tables, AMA DCR and privacy-preserving access-log format required.
Required: WebErrorLog_CL, WebAccessLog_CL.
Reviewed query template
// Log Analytics: requires custom DCR tables with TimeGenerated, RawData, Computer.
// Access format: timestamp|ip|method|uri|status|bytes|content-type|transfer-encoding|cookie-flag
// Only a 0/1/- response-cookie SIZE FLAG is collected, never cookie/session values.
// "-" means unobserved, not small. Malformed records remain visible as parse errors.
let Errors = WebErrorLog_CL
| where TimeGenerated between (ago(14d) .. now())
| where RawData matches regex @"(?i)(Allowed memory size|Out of memory|Maximum execution time|child( pid)? [0-9]+ exit(ed)? (on |with |)signal|child [0-9]+ exited with code [0-9]+ after)"
| summarize Events = count()
by TimeGenerated = bin(TimeGenerated, 10m), ResourceId = _ResourceId, Computer
| extend ClientIp = "", Signal = "web-error-memory-or-worker-failure",
MultipartErrors = long(0), ChunkedMultipart = long(0), LargeResponseCookieFlags = long(0),
UnknownResponseCookie = long(0), MaxRequestBytes = long(0), ParseErrors = long(0);
let Access = WebAccessLog_CL
| where TimeGenerated between (ago(14d) .. now())
| parse RawData with LoggedAt "|" ClientIp "|" Method "|" Uri "|" Status:int "|" RequestBytes:long "|" ContentType "|" TransferEncoding "|" CookieFlag
| extend ParseOk = isnotnull(Status) and Status between (100 .. 599)
and isnotnull(RequestBytes) and RequestBytes >= 0 and CookieFlag in ("0", "1", "-");
let AccessSignals = Access
| where ParseOk
| summarize Events = count(),
MultipartErrors = countif(Method == "POST" and ContentType contains "multipart/form-data" and Status between (500 .. 599)),
ChunkedMultipart = countif(Method == "POST" and ContentType contains "multipart/form-data" and TransferEncoding has "chunked"),
LargeResponseCookieFlags = countif(CookieFlag == "1"), UnknownResponseCookie = countif(CookieFlag == "-"),
MaxRequestBytes = max(RequestBytes)
by TimeGenerated = bin(TimeGenerated, 10m), ResourceId = _ResourceId, Computer, ClientIp
| where MultipartErrors >= 3 or ChunkedMultipart >= 1 or LargeResponseCookieFlags >= 1
| extend Signal = "web-access-multipart-or-response-cookie-flag", ParseErrors = long(0);
let ParseFailures = Access
| where not(ParseOk)
| summarize Events = count()
by TimeGenerated = bin(TimeGenerated, 10m), ResourceId = _ResourceId, Computer
| extend ClientIp = "", Signal = "collection-parse-error-review-required",
MultipartErrors = long(0), ChunkedMultipart = long(0), LargeResponseCookieFlags = long(0),
UnknownResponseCookie = long(0), MaxRequestBytes = long(0), ParseErrors = Events;
union Errors, AccessSignals, ParseFailures
| project TimeGenerated, ResourceId, Computer, ClientIp, Signal, Events,
MultipartErrors, ChunkedMultipart, LargeResponseCookieFlags, UnknownResponseCookie,
MaxRequestBytes, ParseErrors, EvidenceKind = "observed-custom-web-log-review-required"
Actual result: not-applicable / 2026-10-24T18:35:03.365Z; not executed.
This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.
Reviewed result and public fields
{
"id": "web-text-logs",
"platform": "sentinel-log-analytics",
"status": "not-applicable",
"collectedAt": "2026-10-24T18:35:03.365Z",
"rowCount": null,
"reason": "This hunt requires a separately authorized VM, registry/container, Application Gateway or custom-log scope; none is selected by this App Service console.",
"missingTables": [
"WebErrorLog_CL",
"WebAccessLog_CL"
],
"rows": null,
"queryTemplateSha256": "f83b5fe602995145ee5579a900182e312e656dc404fe02d74cdf7aaa9356f37d"
}