azure-apim-linter
v1.0.0
Published
Linting rules for Azure API Management XML policy files and OpenAPI specs. Works as a husky pre-commit hook and in CI/CD pipelines.
Maintainers
Readme
Azure API Management Policy Linter
azure-apim-linter — catch security issues, structural violations, and convention breaches in your APIM XML policy files and OpenAPI specs before they reach your Azure API Management instance.
Works as a husky pre-commit hook (lints only staged files) and as a CI/CD pipeline step (lints changed files on every push).
Why this exists
Azure API Management policies are XML files deployed directly to your APIM instance. A misconfigured policy can:
- Expose hardcoded secrets (API keys, credentials, backend URLs)
- Break the policy inheritance chain (
<base />missing) - Deploy malformed policies that silently fail or cause runtime errors
- Violate team conventions that are impossible to catch in code review at scale
No dedicated tool existed for linting APIM XML policy files with pipeline-aware exit codes. This package fills that gap.
Works best with APIOps
This linter is designed to work alongside the APIOps project. APIOps provides extractor and publisher tools that let you pull your live APIM instance — its APIs, policies, and configuration — into a Git repository. Once your APIM content lives in source control, you can wire azure-apim-linter into your pre-commit hook or CI/CD pipeline to catch issues before they reach your instance.
If you have not set up APIOps yet, start there first:
- APIOps toolkit on GitHub
- Configure the extractor and publisher tools
- Video walkthrough: https://youtu.be/8ZIt_DlNCoo?si=GEGysf6JkVN7bO6l
Installation
npm install --save-dev azure-apim-linterOr run without installing (one-off scan):
npx azure-apim-linter --allQuick start
# Lint all XML and YAML files in your repo
npx azure-apim-linter --all
# Lint specific files
npx azure-apim-linter --file apis/my-api/policy.xml
# Lint only XML (skip OpenAPI/YAML)
npx azure-apim-linter --all --skip-yamlCLI reference
azure-apim-linter [options]
File targeting:
(no flag) Auto-detect git staged files [pre-commit mode]
--file <f1> [f2 ...] Lint specific files [CI/pipeline mode]
--all Lint all XML/YAML files in the current directory tree
Scope:
--skip-xml Skip XML policy linting
--skip-yaml Skip YAML/OpenAPI linting
Config:
--config <path> Path to config file (default: ./apim-linter.config.json)
Other:
--help, -h Show helpExit codes
| Code | Meaning |
|------|---------|
| 0 | All checks passed (warnings do not cause non-zero exit) |
| 1 | One or more errors found |
Configuration
Create an apim-linter.config.json file in your repository root to customise which rules run and at what severity:
{
"rootDir": "artifacts",
"xml": {
"rules": {
"required-tags": "error",
"inbound-base-tag": "error",
"no-hardcoded-functions-key": "error",
"no-hardcoded-set-url": "error",
"send-request-validation": "error",
"return-response-validation": "error",
"no-hardcoded-auth-basic": "error",
"cache-lookup-required": "error",
"backend-id-lowercase": "warn",
"subscription-key-removal": "warn",
"set-variable-camelcase": "warn"
},
"exclude": []
},
"yaml": {
"redoclyConfig": "",
"exclude": []
}
}Rule severities
| Value | Behaviour |
|-------|-----------|
| "error" | Fails the lint run (exit code 1) |
| "warn" | Prints a warning, does not fail |
| "off" | Rule is disabled entirely |
Excluding paths
{
"xml": {
"exclude": ["deprecated/", "third-party/policies/"]
}
}Any file whose path contains one of the exclude strings will be skipped.
Custom Redocly config (YAML linting)
{
"yaml": {
"redoclyConfig": "./my-team-oas-rules.yaml"
}
}If omitted, the bundled oas-linting/oas-rules.yaml is used.
XML rules reference
| Rule | Default | Description |
|------|---------|-------------|
| required-tags | error | The 10 structural APIM policy tags (<policies>, <inbound>, <backend>, <outbound>, <on-error> and their closing forms) must each appear exactly once. Self-closing <backend />, <outbound />, <on-error /> are accepted. Skipped for fragment files. |
| inbound-base-tag | error | <inbound> must contain an uncommented <base /> tag to preserve the policy inheritance chain. Skipped for fragment files. |
| no-hardcoded-functions-key | error | When <set-header name="x-functions-key"> is present, its <value> must use named values ({{ }}) or the context object — not a literal string. |
| no-hardcoded-set-url | error | <set-url> content must reference context or use {{ }} named values. Literal URLs are not permitted. |
| send-request-validation | error | <send-request> and <send-one-way-request> must have: a numeric timeout attribute, a <set-url> child with non-hardcoded content, and a <set-method> child. |
| return-response-validation | error | <set-status code="..."> must be numeric or reference context/@. If <set-body> is present, <set-header> must also be present. Missing <set-body> produces a warning. |
| no-hardcoded-auth-basic | error | <authentication-basic> username and password attributes must use {{ }}, context, or @ — no plaintext credentials. |
| cache-lookup-required | error | Every key used in <cache-store-value> must have a matching <cache-lookup-value> with the same key. Commented-out lines are ignored. |
| backend-id-lowercase | warn | The backend-id attribute on <set-backend-service> must be fully lowercase. |
| subscription-key-removal | warn | Both <set-header name="Ocp-Apim-Subscription-Key" exists-action="delete" /> and <set-query-parameter name="subscription-key" exists-action="delete" /> should be present to strip subscription keys before forwarding to backends. Skipped for fragment files. |
| set-variable-camelcase | warn | The name attribute on <set-variable> must be non-empty and in camelCase. |
Fragment files
Files containing a <fragment> tag are detected automatically. The following rules are skipped for fragment files, since they don't use the full <policies> skeleton:
required-tagsinbound-base-tagsubscription-key-removal
Suppressing lint issues
Sometimes you need to intentionally write code that would normally trigger a rule — for example, a hardcoded URL in a proof-of-concept file. Use apim-linter-disable comments to suppress issues inline, without touching your rule config.
Suppress a block (all rules)
<!-- apim-linter-disable: start -->
<set-url>https://hardcoded-poc-url.example.com</set-url>
<set-method>GET</set-method>
<!-- apim-linter-disable: end -->Everything between the start and end markers is ignored by all rules.
Suppress a block (specific rule only)
<!-- apim-linter-disable no-hardcoded-set-url: start -->
<set-url>https://hardcoded-poc-url.example.com</set-url>
<!-- apim-linter-disable no-hardcoded-set-url: end -->Only the named rule is suppressed inside the block. All other rules still apply to those lines.
Suppress file-level rules
Some rules check the file as a whole rather than a specific line (e.g. required-tags, inbound-base-tag, subscription-key-removal). Suppress them with a disable comment anywhere in the file — the top is the recommended location:
<!-- apim-linter-disable required-tags: start -->
<!-- apim-linter-disable required-tags: end -->
<policies>
...
</policies>An all-rules disable: start anywhere in the file also suppresses all file-level checks.
Unclosed blocks
If a disable: start comment has no matching end, suppression extends to the end of the file. This is intentional — it matches ESLint's lenient behaviour and avoids cryptic partial-suppression states.
Available rule names for targeted suppression
required-tags inbound-base-tag
no-hardcoded-functions-key no-hardcoded-set-url
send-request-validation return-response-validation
no-hardcoded-auth-basic cache-lookup-required
backend-id-lowercase subscription-key-removal
set-variable-camelcaseLocal setup with Husky
Husky runs azure-apim-linter as a pre-commit hook. In staged-file mode (the default, no --file or --all flag), the linter automatically detects which XML/YAML files are staged and only lints those.
1. Install dependencies
Without YAML linting (XML policy linting only):
npm install --save-dev azure-apim-linter huskyWith YAML / OpenAPI linting (recommended full setup):
npm install --save-dev azure-apim-linter husky @redocly/cli2. Initialize Husky
npx husky init3. Set the pre-commit hook
echo "npx azure-apim-linter" > .husky/pre-commit4. (Optional) Create your config
# Copy the default config as a starting point
cp node_modules/azure-apim-linter/apim-linter.config.json ./apim-linter.config.jsonEdit to disable or downgrade rules that don't apply to your setup.
5. Test it
Stage an XML policy file and commit:
git add apis/my-api/policy.xml
git commit -m "test: verify pre-commit hook"The hook runs automatically. If errors are found, the commit is blocked.
Pipeline integration
Azure DevOps
Add these stages to your pipeline YAML. Use APIM_XML_LINT_FLAG and APIM_OAS_LINT_FLAG variable group flags to enable/disable each stage per environment.
stages:
- stage: Lint_XML
condition: eq(variables['APIM_XML_LINT_FLAG'], 'true')
displayName: Lint APIM XML Policies
jobs:
- job: LintXML
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
fetchDepth: 0
- task: NodeTool@0
inputs:
versionSpec: "20.x"
displayName: Install Node.js
- script: npm install azure-apim-linter
displayName: Install azure-apim-linter
- script: |
CHANGED_XML=$(git diff --name-only $(Build.SourceVersion)~1 $(Build.SourceVersion) \
| grep 'policy\.xml$' | tr '\n' ' ')
if [ -z "$CHANGED_XML" ]; then
echo "No policy.xml files changed. Skipping."
exit 0
fi
npx azure-apim-linter --file $CHANGED_XML --skip-yaml
displayName: Run XML Linting
- stage: Lint_OAS
condition: eq(variables['APIM_OAS_LINT_FLAG'], 'true')
displayName: Lint OpenAPI Specs
jobs:
- job: LintOAS
pool:
vmImage: ubuntu-latest
steps:
- checkout: self
fetchDepth: 0
- task: NodeTool@0
inputs:
versionSpec: "20.x"
displayName: Install Node.js
- script: npm install azure-apim-linter @redocly/cli
displayName: Install linter and Redocly
- script: |
CHANGED_YAML=$(git diff --name-only $(Build.SourceVersion)~1 $(Build.SourceVersion) \
| grep 'specification\.yaml$' | tr '\n' ' ')
if [ -z "$CHANGED_YAML" ]; then
echo "No specification.yaml files changed. Skipping."
exit 0
fi
npx azure-apim-linter --file $CHANGED_YAML --skip-xml
displayName: Run OAS LintingGitHub Actions
name: APIM Lint
on:
pull_request:
paths:
- '**/*.xml'
- '**/*.yaml'
jobs:
lint-xml:
name: Lint XML Policies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install azure-apim-linter
- name: Detect changed XML files and lint
run: |
CHANGED_XML=$(git diff --name-only HEAD~1 HEAD | grep 'policy\.xml$' | tr '\n' ' ')
if [ -z "$CHANGED_XML" ]; then
echo "No policy.xml files changed."
exit 0
fi
npx azure-apim-linter --file $CHANGED_XML --skip-yaml
lint-oas:
name: Lint OpenAPI Specs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install azure-apim-linter @redocly/cli
- name: Detect changed YAML files and lint
run: |
CHANGED_YAML=$(git diff --name-only HEAD~1 HEAD | grep 'specification\.yaml$' | tr '\n' ' ')
if [ -z "$CHANGED_YAML" ]; then
echo "No specification.yaml files changed."
exit 0
fi
npx azure-apim-linter --file $CHANGED_YAML --skip-xmlYAML / OpenAPI linting (optional)
YAML linting uses Redocly CLI as an optional peer dependency. A bundled oas-linting/oas-rules.yaml config is included covering the most common OpenAPI spec rules for APIM.
Install Redocly
npm install --save-dev @redocly/cliImportant: Redocly must be installed as a local devDependency in your project. Global installs (
npm install -g @redocly/cli) are not supported. The binary is resolved fromnode_modules/.bin/redoclyin your project directory.The npm registry contains a squatter package simply named
redoclywhich is unrelated to@redocly/cli. Usingnpx redoclyor any global lookup will silently find the wrong package. Always install@redocly/cli(with the@redocly/scope).
Run YAML linting
azure-apim-linter --skip-xmlAll staged (or specified) YAML files are passed to Redocly in a single invocation, which is significantly faster than one process per file for large repos.
Use a custom Redocly config
Set yaml.redoclyConfig in your apim-linter.config.json:
{
"yaml": {
"redoclyConfig": "./my-oas-rules.yaml"
}
}If @redocly/cli is not installed locally, YAML linting is skipped with a warning — it does not cause the run to fail.
Contributing
Pull requests are welcome.
Adding a new XML rule
- Add the check function to
xml-linting/xml-rules.jsfollowing the existing pattern (pure function, takeslinesarray, returns{ line: number, message: string }[]— useline: 0for file-level issues) - Register it in
RULE_MAPwith a rule name, type ("errors","warnings", or"mixed"), and label - Add a default severity to
DEFAULT_CONFIG - Add a test file in
__tests__/xml-rules/ - Document it in the rules table in
README.md
Suppression is handled automatically by lintFile() — individual rule functions do not need to be aware of disable comments.
Running tests
npm test
npm run test:coverageLicense
MIT — Krish Tomar
