= {
- "storage blob download-batch": { flags: ["--destination", "-d"] },
- "storage file download-batch": { flags: ["--destination", "-d"] },
- "storage blob upload-batch": { flags: ["--source", "-s"] },
- "storage file upload-batch": { flags: ["--source", "-s"] },
- "storage fs directory download": { flags: ["--destination-path", "-d"] },
- "storage fs directory upload": { flags: ["--source", "-s"] },
- "webapp deploy": { flags: ["--src-path"] },
- "functionapp deploy": { flags: ["--src-path"] },
- "webapp deployment source config-zip": { flags: ["--src"] },
- "functionapp deployment source config-zip": { flags: ["--src"] },
- "logicapp deployment source config-zip": { flags: ["--src"] },
- "webapp config container set": { flags: ["--multicontainer-config-file"] },
- "webapp create": { flags: ["--multicontainer-config-file"] },
- "container create": { flags: ["--file", "-f"] },
- "container container-group-profile create": { flags: ["--file", "-f"] },
- "aks command invoke": { flags: ["--file", "-f"] },
- "ts export": { flags: ["--output-folder"] },
- "appconfig kv export": { flags: ["--path"] },
- // az opens these itself in custom code, and they always take a path (never inline), so they are
- // checked unconditionally like the rest of this list — a write target need not exist yet, so the
- // existing-path safety net below would miss it. `rest --output-file` writes the
- // response; the `apim` ones read/write a spec or schema file.
- rest: { flags: ["--output-file"] },
- "apim api import": { flags: ["--specification-path"] },
- "apim api export": { flags: ["--file-path", "-f"] },
- "apim api schema create": { flags: ["--schema-path"] },
-};
-
-// ---------------------------------------------------------------------------
-// Policy data. Exported so tests and the cross-check can read the same source.
-// ---------------------------------------------------------------------------
-
-const deny = (
- reason: string,
- ...prefixes: string[][]
-): Array<{ match: string[]; reason: string }> => prefixes.map((match) => ({ match, reason }));
-
-/** Denied command groups and verbs. Prefix matches. */
-export const DENIED: Array<{ match: string[]; reason: string }> = [
- ...deny("the CLI is already logged in to the emulator with a dummy account", ["login"]),
- ...deny("this would break routing to the emulator", ["logout"], ["account", "clear"]),
- ...deny(
- "this would re-point the CLI away from the emulator",
- ["cloud", "register"],
- ["cloud", "unregister"],
- ["cloud", "update"],
- ["cloud", "set"]
- ),
- // `config get` is allowed below; every other `config`, plus `configure`/`init`, writes state.
- ...deny("the tool manages the CLI configuration", ["config"], ["configure"], ["init"]),
- ...deny(
- "extensions are pre-installed; adding or updating one downloads from the internet",
- ["extension", "add"],
- ["extension", "update"],
- ["extension", "remove"]
- ),
- ...deny(
- "this downloads or installs software on this machine",
- ["upgrade"],
- ["bicep", "install"],
- ["bicep", "upgrade"],
- ["bicep", "uninstall"],
- ["bicep", "list-versions"],
- ["aks", "install-cli"] // also runs `setx path`, rewriting the real user PATH
- ),
- ...deny(
- "Bicep registry modules cannot be reached from the local emulator",
- ["bicep", "restore"],
- ["bicep", "publish"]
- ),
- ...deny("this calls a Microsoft web service", ["find"], ["feedback"], ["survey"]),
- ...deny("this needs an interactive session", ["interactive"], ["self-test"]),
- ...deny(
- "this opens a browser on this machine",
- ["aks", "browse"],
- ["webapp", "browse"],
- ["containerapp", "browse"]
- ),
- ...deny(
- "this opens a shell, tunnel or live stream",
- ["webapp", "ssh"],
- ["webapp", "create-remote-connection"],
- ["container", "exec"],
- ["container", "attach"],
- ["containerapp", "exec"],
- ["webapp", "log", "tail"],
- ["appservice", "plan", "managed-instance", "instance", "connect"],
- ["network", "bastion", "ssh"],
- ["network", "bastion", "rdp"],
- ["network", "bastion", "tunnel"]
- ),
- ...deny(
- "this runs Docker or downloads tools on this machine",
- ["acr", "check-health"],
- ["acr", "helm"],
- ["aks", "check-acr"],
- ["storage", "copy"],
- ["storage", "remove"],
- ["storage", "blob", "sync"], // storage copy/remove/sync auto-install azcopy from aka.ms
- ["backup", "restore", "files", "mount-rp"],
- ["mysql", "flexible-server", "deploy"],
- ["postgres", "flexible-server", "deploy"]
- ),
- // Denied outright until its local-source flag is wired from _params.py. With a
- // local source it makes az run `docker build`/`push` on the shared engine.
- ...deny("this builds and pushes images on the shared Docker engine", [
- "cognitiveservices",
- "agent",
- "create",
- ]),
- ...deny(
- "Azure DevOps is not emulated",
- ["devops"],
- ["boards"],
- ["repos"],
- ["pipelines"],
- ["artifacts"]
- ),
-];
-
-/** Verbs that survive a broader group denial. */
-export const ALLOWED_EXCEPTIONS: string[][] = [["config", "get"]];
-
-/** Flags that are refused, globally or for a specific command. */
-export const DENIED_FLAGS: Array<{ command?: string[]; flag: string; reason: string }> = [
- { flag: "--follow", reason: "it streams output until killed" },
- { flag: "--login-with-github", reason: "it opens a browser login" },
- { command: ["webapp", "up"], flag: "--launch-browser", reason: "it opens a browser" },
- { command: ["webapp", "up"], flag: "-b", reason: "it opens a browser" },
- { command: ["webapp", "up"], flag: "--logs", reason: "it streams logs until killed" },
- {
- command: ["containerapp", "up"],
- flag: "--source",
- reason: "it builds an image on the shared Docker engine",
- },
- {
- command: ["containerapp", "up"],
- flag: "--repo",
- reason: "it wires up a GitHub Actions build",
- },
- {
- command: ["containerapp", "create"],
- flag: "--source",
- reason: "it builds an image on the shared Docker engine",
- },
- {
- command: ["containerapp", "create"],
- flag: "--repo",
- reason: "it wires up a GitHub Actions build",
- },
-];
-
-/** Flags a command must carry, or it is refused. */
-export const REQUIRED_FLAGS: Array<{
- command: string[];
- flag: string;
- aliases?: string[];
- reason: string;
-}> = [
- {
- command: ["acr", "login"],
- flag: "--expose-token",
- aliases: ["-t"],
- reason:
- "without `--expose-token` az runs `docker login` on this machine; re-run it with `--expose-token`",
- },
-];
-
-/**
- * Local hosts the URL rule and the egress guard both allow: one
- * shared check in local-hosts.ts. Re-exported here for the policy's own tests.
- */
-export { LOCAL_HOST };
-
-/** The ARM host whose absolute URLs are rewritten to a relative path (matched case-insensitively). */
-const MANAGEMENT_HOST = "management.azure.com";
-
-/** URL-target flags whose value az turns into a request or a download. */
-const REST_URL_FLAGS = new Set(["--url", "--uri", "-u"]); // only for the `rest` command
-const FETCH_URL_FLAGS = new Set(["--template-uri", "--multicontainer-config-file"]); // any command
-const PARAMETERS_FLAGS = new Set(["--parameters", "-p"]); // URL or file, depending on the value
-
-// ---------------------------------------------------------------------------
-// Denylist file parsing.
-// ---------------------------------------------------------------------------
-
-/**
- * Parse LOCALSTACK_AZ_DENYLIST_FILE: one command prefix per line, split on whitespace. Blank
- * lines and `#` comments are ignored. The result is passed to evaluateAzCommand
- * as opts.extraDenied, so the policy itself stays pure.
- */
-export function parseDenylistFile(text: string): string[][] {
- const prefixes: string[][] = [];
- for (const rawLine of text.split(/\r?\n/)) {
- const withoutComment = rawLine.replace(/#.*$/, "");
- const words = withoutComment.trim().split(/\s+/).filter(Boolean);
- if (words.length > 0) prefixes.push(words);
- }
- return prefixes;
-}
-
-// ---------------------------------------------------------------------------
-// Small helpers.
-// ---------------------------------------------------------------------------
-
-const START_TOKEN = /^[a-z][a-z0-9-]*$/;
-const HELP_TOKENS = new Set(["--help", "-h"]);
-const ARM_ID = /^\/(subscriptions|providers|tenants)\//i;
-const ABSOLUTE_URL = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
-
-function effectivePlatform(opts: PolicyOptions): NodeJS.Platform {
- return opts.platform ?? process.platform;
-}
-
-/** True when a command's leading words match `prefix` exactly. */
-function hasPrefix(argv: string[], prefix: string[]): boolean {
- if (prefix.length > argv.length) return false;
- return prefix.every((word, index) => argv[index] === word);
-}
-
-/**
- * Match an option token against a canonical flag, honouring argparse's unambiguous-prefix
- * abbreviation for long flags (verified: `cloud list --out tsv` and `--que` both work). A token
- * `--` with p.length >= 2 that is a strict prefix of a long flag counts as that flag; short
- * `-x` flags match only exactly. When a prefix could match several path/URL flags we still apply
- * the rule (argparse would reject it as ambiguous, so being stricter is safe).
- */
-function flagMatches(tokenFlag: string, fullFlag: string): boolean {
- if (tokenFlag === fullFlag) return true;
- return (
- fullFlag.startsWith("--") &&
- tokenFlag.startsWith("--") &&
- tokenFlag.length >= 4 && // "--" plus at least two characters
- fullFlag.length > tokenFlag.length &&
- fullFlag.startsWith(tokenFlag)
- );
-}
-
-/** True when a token's flag matches any flag in the set (exact or an unambiguous prefix). */
-function matchesAnyFlag(tokenFlag: string, flags: Iterable): boolean {
- for (const flag of flags) if (flagMatches(tokenFlag, flag)) return true;
- return false;
-}
-
-interface ParsedOption {
- flag: string;
- /** The value packed into the token itself (`--flag=v`, `-fv`, `-f=v`), else undefined. */
- value?: string;
- /** How an inline value was attached, so a rewrite can rebuild the token. */
- sep: "=" | "";
-}
-
-/** Split one option token into its flag and any attached value (no lookup of the next token). */
-function parseOption(token: string): ParsedOption {
- if (token.startsWith("--")) {
- const eq = token.indexOf("=");
- if (eq >= 0) return { flag: token.slice(0, eq), value: token.slice(eq + 1), sep: "=" };
- return { flag: token, sep: "" };
- }
- // A single-dash short option, possibly with a stuck or `=`-joined value (`-f/x`, `-f=/x`).
- const flag = token.slice(0, 2);
- const rest = token.slice(2);
- if (rest === "") return { flag, sep: "" };
- if (rest.startsWith("=")) return { flag, value: rest.slice(1), sep: "=" };
- return { flag, value: rest, sep: "" };
-}
-
-/** The path-taking flags of the command in `argv`, from the generated table plus the supplement. */
-function commandFileFlags(argv: string[]): {
- generic: Set;
- greedy: Set;
- params: Set;
- positional: boolean;
-} {
- const generic = new Set();
- const greedy = new Set