cd ../blog

There Is a SARIF File Inside Your Canvas App. Put It in Your CI Pipeline.

Plus the 83 preview feature flags your app is carrying, and what they reveal about what is coming.

</>

There Is a SARIF File Inside Your Canvas App. Put It in Your CI Pipeline.

Plus the 83 preview feature flags your app is carrying, and what they reveal about what is coming.


Unpack an .msapp and you will find this at the root:

AppCheckerResult.sarif

SARIF — Static Analysis Results Interchange Format — is the OASIS standard for static analysis output. It is what CodeQL emits. It is what GitHub code scanning ingests. It is what Azure DevOps’ scanning tab reads.

Microsoft’s App Checker writes its results into your app file in that exact format, and almost nobody knows it is there.

What is in it

The app we examined — 7 screens, 994 controls — carries 402 findings across 14 rules:

CountRule
254acc-AccessibleLabelNeeded
27app-ErrInvalidName
27app-ErrInvalidArgs-Func
21app-InefficientDelayLoading
20acc-TabIndexShouldBeDefinedForInteractiveControl
19app-ErrInvalidDot
11app-ErrUnknownNamespaceFunction
10app-ErrTypeError
5acc-FocusBorderShouldBeVisible
3app-ErrIncompatibleTypesForEquality-Left-Right
2app-NumericPropertySizeWarning
1app-SuggestRemoteExecutionHint-OpNotSupportedByService
1acc-ReadableScreenNameNeeded
1app-ScreenHasManyControls

254 missing accessible labels. In one app. That is 63% of all findings, and it is the single number that should end any argument about whether canvas apps have an accessibility problem.

The acc- prefix marks accessibility rules; app- marks correctness and performance. Four of the fourteen rules are accessibility, and they account for 280 of the 402 findings.

Some of the others are more interesting than they look:

  • app-ErrInvalidName (27) and app-ErrInvalidDot (19) are formulas referencing things that do not exist. This app has 46 broken references and ships anyway.
  • app-InefficientDelayLoading (21) is the platform telling you your delay-load configuration is not helping.
  • app-SuggestRemoteExecutionHint-OpNotSupportedByService is a delegation warning — the one class of Power Apps problem that silently returns wrong data rather than failing loudly.
  • app-ScreenHasManyControls fired exactly once, on the 495-control screen. So the platform does have a threshold, and this app crossed it once.

The app’s own metadata corroborates all of it:

"ParserErrorCount":  1,
"BindingErrorCount": 97

97 binding errors, recorded at save time, in a published production app.

A second app, and the number gets worse

We ran the same extraction on a larger app — 1,607 controls, saved six months later. 828 findings across 11 rules, and the mix is different:

CountRule
351acc-AccessibleLabelNeeded
229acc-TabIndexShouldBeDefinedForInteractiveControl
180acc-FocusBorderShouldBeVisible
25app-InefficientDelayLoading
18app-NumericPropertySizeWarning
17app-WarnLiteralPredicate
3app-ScreenHasManyControls
2app-CollectingReadOnlyTable
1app-DataSourceDefaultMaxRowsLimit
1app-CountRowsGalleryAllItems
1app-CrossScreenEventDependencies

760 of 828 findings — 92% — are accessibility. In the first app it was 280 of 402 (70%). Neither app is unusual; both are ordinary line-of-business software.

Two other things worth noting. This app has zero app-Err* findings, matching its BindingErrorCount: 0 — cleaner than the first app by every correctness measure, and still carrying 760 accessibility problems. And five rules appear here that the first app never triggered — app-WarnLiteralPredicate, app-CollectingReadOnlyTable, app-DataSourceDefaultMaxRowsLimit, app-CountRowsGalleryAllItems, app-CrossScreenEventDependencies — so the rule set is larger than any single app reveals. Do not treat one app’s SARIF as the full catalogue of what App Checker knows.

app-ScreenHasManyControls fired three times here, against once in the smaller app.

Why the SARIF format matters

Because it means zero integration work.

The findings are already in the standard shape: runs[0].tool.driver.rules[] holds the rule definitions, runs[0].results[] holds each finding with its rule id and location. GitHub’s code scanning API accepts SARIF directly. So does Azure DevOps. So does every commercial security dashboard.

A complete quality gate for canvas apps is roughly this:

unzip -q -o app.msapp -d app/
gh api /repos/:owner/:repo/code-scanning/sarifs \
  -f commit_sha="$GITHUB_SHA" -f ref="refs/heads/main" \
  -f sarif="$(gzip -c app/AppCheckerResult.sarif | base64 -w0)"

Findings appear in the Security tab of your repo, annotated, with history and trend lines. No parser to write, no schema to learn.

That gives you things the Power Platform tooling does not currently offer: a finding count per commit, a diff of new findings introduced by a change, and a merge gate that fails when accessibility findings increase. If you keep your apps in source control — via pac canvas unpack or an export pipeline — this is a couple of hours of work for a permanent quality signal.

One caveat worth stating: the SARIF is written when the app is saved from Studio, not on demand. It reflects the state at last save, so treat it as a snapshot rather than live analysis. If you need it fresh, save the app.

The other file worth reading: 83 preview flags

Properties.json contains a field called AppPreviewFlagsMap. In this app it holds 83 flags — 41 on, 42 off. It is the most direct look you will get at what the platform team is building.

Some of the flags that are on:

FlagWhat it appears to govern
enablecanvasappruntimecopilotCopilot in the app runtime
fluentv9controlspreviewFluent v9 control preview
enablepcfmoderndatasetsmodern datasets for PCF controls
dataflowanalysisenabledthe dataflow analysis pass (also in Header.json)
delayloadscreens, delaycontrolrenderingstaged loading
errorhandlingthe error-handling feature set
userdefinedtypesuser-defined types in Power Fx
enablecreateaformulaformula authoring assistance
useexperimentalsqlconnector, useexperimentalcdsconnector, nativecdsexperimentalexperimental connectors
supportcolumnnamesasidentifiers, useguiddatatypes, consistentreturnschemafortabularfunctionsPower Fx semantics changes
enableupdateifdelegationdelegation for UpdateIf
proactivecontrolrenamerename propagation
zeroalltabindexesforces all tab indexes to zero

And some that are off:

FlagWhy it stands out
powerfxv1Power Fx v1 is off. This app runs pre-v1 language semantics.
powerfxdecimaldecimal number support, off
fluentv9controlsthe non-preview Fluent v9 flag, off — while fluentv9controlspreview is on
classiccontrolsoff
herocontrolsunannounced; the name suggests a new control family
onegridunannounced; the name suggests a unified grid control
mobilenativerenderingnative rendering on mobile
packagemodernruntimea modern runtime, packaged with the app
enabledataverseofflineDataverse offline
adaptivepaging, longlivingcache, keeprecentscreensloadedperformance work
improvedtabstopbehavioraccessibility work
rtlsupport, rtlinstudiopreviewright-to-left support
dynamicschema, disableruntimepolicies, generatedebugpublishedappplatform internals
enablecopilotcontrol, enablecopilotanswercontrolCopilot controls, off

Four things worth pulling out of that.

powerfxv1 is off. Whatever you believe about which Power Fx semantics your app runs under, the flag says pre-v1. Two rows down, powerfxdecimal is off too. If you have chased an arithmetic or type-coercion oddity, check these before assuming the docs describe your runtime.

herocontrols and onegrid are names we have not seen publicly. Both off. Both sound like control families rather than tweaks.

fluentv9controls off / fluentv9controlspreview on is a paired flag mid-rollout — preview channel enabled, general one not.

These are per-app, not per-tenant. The flag map is serialised into the app file, which means two apps in the same environment can run different language semantics and different control behaviour. That is a real and rarely-discussed source of “it works in my app but not yours.”

The map grows over time. The later app carries 85 flags against 83, with two additions — showm365copilot and disablem365copilot — and, notably, not a single flag flipped value between the two exports six months apart. powerfxv1 and powerfxdecimal are still off, fluentv9controls still off while fluentv9controlspreview stays on. New capability arrives as new flags; the existing defaults barely move.

And the version gate nobody mentions

Header.json is four fields long and one of them can lock you out of your own app:

{
  "DocVersion": 1.348,
  "MinVersionToLoad": 1.348,
  "MSAppStructureVersion": "2.4.0",
  "LastSavedDateTimeUTC": "..."
}

MinVersionToLoad is the floor. A player or Studio older than that number refuses to open the file. In this app it equals DocVersion exactly — the app was saved by a Studio that bumped the floor to its own version, and nothing older can open it now.

OriginatingVersion in Properties.json says 1.346, two versions behind. So the app was created on 1.346, saved on 1.348, and that save raised its minimum.

That is the mechanism behind “the app opens for me but not for my colleague” and behind apps that will not import into an environment on an older release ring. It is one number, it only ever goes up, and there is no UI for it.

The short version

Three files in every .msapp, none documented as a tooling surface, all immediately useful:

  • AppCheckerResult.sarif — 402 findings in standard SARIF, ready to pipe into GitHub code scanning today.
  • Properties.jsonAppPreviewFlagsMap — 83 flags, per-app, telling you which language and control semantics your app actually runs under.
  • Header.jsonMinVersionToLoad — the version floor that decides who can open your app.

An .msapp is a ZIP. Go look.


Methodology

Sample: one production .msapp exported 2026-02-16 (DocVersion 1.348, MSAppStructureVersion 2.4.0).

unzip -q app.msapp -d app/
python3 - <<'EOF'
import json, collections
s = json.load(open('app/AppCheckerResult.sarif'))['runs'][0]
print('rules:', len(s['tool']['driver']['rules']), 'findings:', len(s['results']))
for rid, n in collections.Counter(r['ruleId'] for r in s['results']).most_common():
    print(f'{n:>5}  {rid}')
p = json.load(open('app/Properties.json'))
f = p['AppPreviewFlagsMap']
print('\nflags:', len(f), '| on:', sum(f.values()), '| off:', len(f) - sum(f.values()))
EOF

Only rule ids and counts are reported — finding locations are omitted, since those contain customer formulas and control names. Flag meanings are inferred from flag names; Microsoft does not publish this list, so treat the names as evidence and the interpretations as informed guesses.