npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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:


Installation

npm install --save-dev azure-apim-linter

Or run without installing (one-off scan):

npx azure-apim-linter --all

Quick 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-yaml

CLI 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 help

Exit 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-tags
  • inbound-base-tag
  • subscription-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-camelcase

Local 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 husky

With YAML / OpenAPI linting (recommended full setup):

npm install --save-dev azure-apim-linter husky @redocly/cli

2. Initialize Husky

npx husky init

3. Set the pre-commit hook

echo "npx azure-apim-linter" > .husky/pre-commit

4. (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.json

Edit 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 Linting

GitHub 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-xml

YAML / 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/cli

Important: 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 from node_modules/.bin/redocly in your project directory.

The npm registry contains a squatter package simply named redocly which is unrelated to @redocly/cli. Using npx redocly or any global lookup will silently find the wrong package. Always install @redocly/cli (with the @redocly/ scope).

Run YAML linting

azure-apim-linter --skip-xml

All 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

  1. Add the check function to xml-linting/xml-rules.js following the existing pattern (pure function, takes lines array, returns { line: number, message: string }[] — use line: 0 for file-level issues)
  2. Register it in RULE_MAP with a rule name, type ("errors", "warnings", or "mixed"), and label
  3. Add a default severity to DEFAULT_CONFIG
  4. Add a test file in __tests__/xml-rules/
  5. 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:coverage

License

MIT — Krish Tomar