@blogic-cz/agent-tools 0.18.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -6
- package/dist/az-tool/config.d.ts +60 -12
- package/dist/az-tool/config.d.ts.map +1 -1
- package/dist/az-tool/errors.d.ts +10 -1
- package/dist/az-tool/errors.d.ts.map +1 -1
- package/dist/az-tool/profile.d.ts +16 -0
- package/dist/az-tool/profile.d.ts.map +1 -0
- package/dist/az-tool/security.d.ts +20 -3
- package/dist/az-tool/security.d.ts.map +1 -1
- package/dist/az-tool/service.d.ts +9 -4
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/az-tool/types.d.ts +10 -60
- package/dist/az-tool/types.d.ts.map +1 -1
- package/dist/{az-tool → azdo-tool}/build.d.ts +9 -9
- package/dist/azdo-tool/build.d.ts.map +1 -0
- package/dist/azdo-tool/config.d.ts +13 -0
- package/dist/azdo-tool/config.d.ts.map +1 -0
- package/dist/azdo-tool/errors.d.ts +43 -0
- package/dist/azdo-tool/errors.d.ts.map +1 -0
- package/dist/azdo-tool/extract-option-value.d.ts.map +1 -0
- package/dist/azdo-tool/index.d.ts +3 -0
- package/dist/azdo-tool/index.d.ts.map +1 -0
- package/dist/azdo-tool/security.d.ts +4 -0
- package/dist/azdo-tool/security.d.ts.map +1 -0
- package/dist/azdo-tool/service.d.ts +15 -0
- package/dist/azdo-tool/service.d.ts.map +1 -0
- package/dist/azdo-tool/transformers.d.ts.map +1 -0
- package/dist/azdo-tool/types.d.ts +64 -0
- package/dist/azdo-tool/types.d.ts.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/loader.d.ts +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/types.d.ts +18 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/shared/azure-credentials.d.ts +20 -0
- package/dist/shared/azure-credentials.d.ts.map +1 -0
- package/dist/shared/binary-preflight.d.ts +1 -1
- package/package.json +8 -2
- package/schemas/agent-tools.schema.json +42 -1
- package/src/az-tool/config.ts +135 -25
- package/src/az-tool/errors.ts +14 -1
- package/src/az-tool/index.ts +67 -134
- package/src/az-tool/profile.ts +48 -0
- package/src/az-tool/security.ts +248 -89
- package/src/az-tool/service.ts +137 -242
- package/src/az-tool/types.ts +11 -64
- package/src/{az-tool → azdo-tool}/build.ts +11 -11
- package/src/azdo-tool/config.ts +33 -0
- package/src/azdo-tool/errors.ts +41 -0
- package/src/azdo-tool/index.ts +222 -0
- package/src/azdo-tool/security.ts +157 -0
- package/src/azdo-tool/service.ts +322 -0
- package/src/azdo-tool/types.ts +67 -0
- package/src/config/index.ts +1 -0
- package/src/config/loader.ts +10 -1
- package/src/config/types.ts +19 -1
- package/src/credential-guard/index.ts +7 -2
- package/src/shared/azure-credentials.ts +50 -0
- package/src/shared/binary-preflight.ts +1 -1
- package/dist/az-tool/build.d.ts.map +0 -1
- package/dist/az-tool/extract-option-value.d.ts.map +0 -1
- package/dist/az-tool/transformers.d.ts.map +0 -1
- /package/dist/{az-tool → azdo-tool}/extract-option-value.d.ts +0 -0
- /package/dist/{az-tool → azdo-tool}/transformers.d.ts +0 -0
- /package/src/{az-tool → azdo-tool}/extract-option-value.ts +0 -0
- /package/src/{az-tool → azdo-tool}/transformers.ts +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
export class AzdoSecurityError extends Schema.TaggedError<AzdoSecurityError>()(
|
|
4
|
+
"AzdoSecurityError",
|
|
5
|
+
{
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
command: Schema.String,
|
|
8
|
+
hint: Schema.optionalKey(Schema.String),
|
|
9
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
10
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
11
|
+
},
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
export class AzdoCommandError extends Schema.TaggedError<AzdoCommandError>()("AzdoCommandError", {
|
|
15
|
+
message: Schema.String,
|
|
16
|
+
command: Schema.String,
|
|
17
|
+
exitCode: Schema.optionalKey(Schema.Number),
|
|
18
|
+
stderr: Schema.optionalKey(Schema.String),
|
|
19
|
+
hint: Schema.optionalKey(Schema.String),
|
|
20
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
21
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
22
|
+
}) {}
|
|
23
|
+
|
|
24
|
+
export class AzdoTimeoutError extends Schema.TaggedError<AzdoTimeoutError>()("AzdoTimeoutError", {
|
|
25
|
+
message: Schema.String,
|
|
26
|
+
command: Schema.String,
|
|
27
|
+
timeoutMs: Schema.Number,
|
|
28
|
+
hint: Schema.optionalKey(Schema.String),
|
|
29
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
30
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
31
|
+
}) {}
|
|
32
|
+
|
|
33
|
+
export class AzdoParseError extends Schema.TaggedError<AzdoParseError>()("AzdoParseError", {
|
|
34
|
+
message: Schema.String,
|
|
35
|
+
rawOutput: Schema.String,
|
|
36
|
+
hint: Schema.optionalKey(Schema.String),
|
|
37
|
+
nextCommand: Schema.optionalKey(Schema.String),
|
|
38
|
+
retryable: Schema.optionalKey(Schema.Boolean),
|
|
39
|
+
}) {}
|
|
40
|
+
|
|
41
|
+
export type AzdoError = AzdoSecurityError | AzdoCommandError | AzdoTimeoutError | AzdoParseError;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
3
|
+
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
|
+
import { Effect, Layer, Option } from "effect";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
makeSchemaCommand,
|
|
8
|
+
formatAny,
|
|
9
|
+
formatOption,
|
|
10
|
+
formatOutput,
|
|
11
|
+
logText,
|
|
12
|
+
renderCauseToStderr,
|
|
13
|
+
VERSION,
|
|
14
|
+
} from "#shared";
|
|
15
|
+
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
16
|
+
import {
|
|
17
|
+
findFailedJobs,
|
|
18
|
+
getBuildJobSummary,
|
|
19
|
+
getBuildLogContent,
|
|
20
|
+
getBuildLogs,
|
|
21
|
+
getBuildTimeline,
|
|
22
|
+
} from "./build";
|
|
23
|
+
import { AzdoService, AzdoServiceLayer } from "./service";
|
|
24
|
+
import { ConfigServiceLayer } from "#config";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Common flags shared across build subcommands
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const commonBuildFlags = {
|
|
31
|
+
format: formatOption,
|
|
32
|
+
profile: Flag.optional(Flag.string("profile")).pipe(
|
|
33
|
+
Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Build subcommands
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const buildTimelineCommand = Command.make(
|
|
42
|
+
"timeline",
|
|
43
|
+
{
|
|
44
|
+
...commonBuildFlags,
|
|
45
|
+
buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
|
|
46
|
+
},
|
|
47
|
+
({ buildId, format, profile: _profile }) =>
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const result = yield* getBuildTimeline(buildId);
|
|
50
|
+
yield* logText(formatAny(result, format));
|
|
51
|
+
}),
|
|
52
|
+
).pipe(Command.withDescription("Get build timeline with all records (jobs, stages, tasks)"));
|
|
53
|
+
|
|
54
|
+
const buildFailedJobsCommand = Command.make(
|
|
55
|
+
"failed-jobs",
|
|
56
|
+
{
|
|
57
|
+
...commonBuildFlags,
|
|
58
|
+
buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
|
|
59
|
+
},
|
|
60
|
+
({ buildId, format, profile: _profile }) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const failedJobs = yield* findFailedJobs(buildId);
|
|
63
|
+
yield* logText(formatAny({ buildId, failedJobs }, format));
|
|
64
|
+
}),
|
|
65
|
+
).pipe(Command.withDescription("Find failed or canceled jobs in a build"));
|
|
66
|
+
|
|
67
|
+
const buildLogsCommand = Command.make(
|
|
68
|
+
"logs",
|
|
69
|
+
{
|
|
70
|
+
...commonBuildFlags,
|
|
71
|
+
buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
|
|
72
|
+
},
|
|
73
|
+
({ buildId, format, profile: _profile }) =>
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const result = yield* getBuildLogs(buildId);
|
|
76
|
+
yield* logText(formatAny(result, format));
|
|
77
|
+
}),
|
|
78
|
+
).pipe(Command.withDescription("Get list of build logs"));
|
|
79
|
+
|
|
80
|
+
const buildLogContentCommand = Command.make(
|
|
81
|
+
"log-content",
|
|
82
|
+
{
|
|
83
|
+
...commonBuildFlags,
|
|
84
|
+
buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
|
|
85
|
+
logId: Flag.integer("log-id").pipe(Flag.withDescription("Log ID")),
|
|
86
|
+
},
|
|
87
|
+
({ buildId, format, logId, profile: _profile }) =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const content = yield* getBuildLogContent(buildId, logId);
|
|
90
|
+
yield* logText(formatAny({ buildId, logId, content }, format));
|
|
91
|
+
}),
|
|
92
|
+
).pipe(Command.withDescription("Get specific log content by log ID"));
|
|
93
|
+
|
|
94
|
+
const buildSummaryCommand = Command.make(
|
|
95
|
+
"summary",
|
|
96
|
+
{
|
|
97
|
+
...commonBuildFlags,
|
|
98
|
+
buildId: Flag.integer("build-id").pipe(Flag.withDescription("Build ID")),
|
|
99
|
+
},
|
|
100
|
+
({ buildId, format, profile: _profile }) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const summary = yield* getBuildJobSummary(buildId);
|
|
103
|
+
yield* logText(formatAny({ buildId, summary }, format));
|
|
104
|
+
}),
|
|
105
|
+
).pipe(Command.withDescription("Get job summaries with duration and status information"));
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Build parent command
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
const buildCommand = Command.make("build", {}).pipe(
|
|
112
|
+
Command.withDescription("Build helpers for Azure DevOps pipelines"),
|
|
113
|
+
Command.withSubcommands([
|
|
114
|
+
buildTimelineCommand,
|
|
115
|
+
buildFailedJobsCommand,
|
|
116
|
+
buildLogsCommand,
|
|
117
|
+
buildLogContentCommand,
|
|
118
|
+
buildSummaryCommand,
|
|
119
|
+
]),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Raw command subcommand (preserves existing --cmd behavior)
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const cmdCommand = Command.make(
|
|
127
|
+
"cmd",
|
|
128
|
+
{
|
|
129
|
+
profile: Flag.optional(Flag.string("profile")).pipe(
|
|
130
|
+
Flag.withDescription("Azure DevOps profile name (from agent-tools config)"),
|
|
131
|
+
),
|
|
132
|
+
project: Flag.optional(Flag.string("project")).pipe(
|
|
133
|
+
Flag.withDescription("Azure DevOps project name (overrides config default)"),
|
|
134
|
+
),
|
|
135
|
+
cmd: Flag.string("cmd").pipe(Flag.withDescription("az command (without 'az' prefix)")),
|
|
136
|
+
dryRun: Flag.boolean("dry-run").pipe(
|
|
137
|
+
Flag.withDescription("Show command without executing"),
|
|
138
|
+
Flag.withDefault(false),
|
|
139
|
+
),
|
|
140
|
+
format: formatOption,
|
|
141
|
+
},
|
|
142
|
+
({ profile: _profile, project, cmd, dryRun, format }) =>
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const projectName = project ? Option.getOrUndefined(project) : undefined;
|
|
145
|
+
|
|
146
|
+
if (dryRun) {
|
|
147
|
+
const fullCommand = `az ${cmd}`;
|
|
148
|
+
yield* logText(`[DRY-RUN] Would execute: ${fullCommand}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const az = yield* AzdoService;
|
|
153
|
+
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
const output = yield* az.runCommand(cmd, projectName);
|
|
156
|
+
const executionTimeMs = Date.now() - startTime;
|
|
157
|
+
|
|
158
|
+
yield* logText(
|
|
159
|
+
formatOutput(
|
|
160
|
+
{
|
|
161
|
+
success: true,
|
|
162
|
+
data: output,
|
|
163
|
+
executionTimeMs,
|
|
164
|
+
},
|
|
165
|
+
format,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}),
|
|
169
|
+
).pipe(
|
|
170
|
+
Command.withDescription(
|
|
171
|
+
`Execute raw az CLI commands directly.
|
|
172
|
+
|
|
173
|
+
EXAMPLES:
|
|
174
|
+
agent-tools-azdo cmd --cmd "pipelines list"
|
|
175
|
+
agent-tools-azdo cmd --cmd "repos list" --project my-project
|
|
176
|
+
agent-tools-azdo cmd --cmd "pipelines runs list --output json"`,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Main command with subcommands
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
const commandsCommand = makeSchemaCommand(() => mainCommand);
|
|
185
|
+
|
|
186
|
+
const mainCommand = Command.make("azdo-tool", {}).pipe(
|
|
187
|
+
Command.withDescription(
|
|
188
|
+
`Azure DevOps Tool for Coding Agents (READ-ONLY)
|
|
189
|
+
|
|
190
|
+
For Azure platform (PaaS) resources — vm, webapp, storage, aks, keyvault — use az-tool instead.
|
|
191
|
+
|
|
192
|
+
Typed build subcommands:
|
|
193
|
+
azdo-tool build timeline --build-id 123
|
|
194
|
+
azdo-tool build failed-jobs --build-id 123
|
|
195
|
+
azdo-tool build logs --build-id 123
|
|
196
|
+
azdo-tool build log-content --build-id 123 --log-id 45
|
|
197
|
+
azdo-tool build summary --build-id 123
|
|
198
|
+
|
|
199
|
+
Raw az wrapper:
|
|
200
|
+
azdo-tool cmd --cmd "pipelines list"`,
|
|
201
|
+
),
|
|
202
|
+
Command.withSubcommands([buildCommand, cmdCommand, commandsCommand]),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const cli = Command.run(mainCommand, {
|
|
206
|
+
version: VERSION,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const MainLayer = AzdoServiceLayer.pipe(
|
|
210
|
+
Layer.provideMerge(ConfigServiceLayer),
|
|
211
|
+
Layer.provideMerge(BunServices.layer),
|
|
212
|
+
Layer.provideMerge(AuditServiceLayer),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const program = withAudit("azdo", cli).pipe(
|
|
216
|
+
Effect.provide(MainLayer),
|
|
217
|
+
Effect.tapCause(renderCauseToStderr),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
BunRuntime.runMain(program, {
|
|
221
|
+
disableErrorReporting: true,
|
|
222
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { InvokeParams, SecurityCheckResult } from "./types";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ALLOWED_SUBCOMMANDS,
|
|
5
|
+
BLOCKED_SUBCOMMANDS,
|
|
6
|
+
ALLOWED_INVOKE_AREAS,
|
|
7
|
+
ALLOWED_INVOKE_RESOURCES,
|
|
8
|
+
BLOCKED_INVOKE_AREAS,
|
|
9
|
+
BLOCKED_INVOKE_RESOURCES,
|
|
10
|
+
STANDALONE_AZ_COMMANDS,
|
|
11
|
+
} from "./config";
|
|
12
|
+
import { extractOptionValue } from "./extract-option-value";
|
|
13
|
+
import { isCredentialBlockedSegment, isCredentialBlockedVerb } from "#shared/azure-credentials";
|
|
14
|
+
|
|
15
|
+
export function isCommandAllowed(cmd: string): SecurityCheckResult {
|
|
16
|
+
const rawCommandWords = cmd.trim().split(/\s+/);
|
|
17
|
+
const commandWords = cmd
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.split(/\s+/)
|
|
21
|
+
.filter((w) => !w.startsWith("-"));
|
|
22
|
+
|
|
23
|
+
if (commandWords.includes("invoke")) {
|
|
24
|
+
const area = extractOptionValue(rawCommandWords, "--area");
|
|
25
|
+
const resource = extractOptionValue(rawCommandWords, "--resource");
|
|
26
|
+
|
|
27
|
+
if (!area || !resource) {
|
|
28
|
+
return {
|
|
29
|
+
allowed: false,
|
|
30
|
+
command: cmd,
|
|
31
|
+
reason: "Invoke command requires both --area and --resource options.",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const method = extractOptionValue(rawCommandWords, "--http-method");
|
|
36
|
+
|
|
37
|
+
if (method && method.toLowerCase() !== "get") {
|
|
38
|
+
return {
|
|
39
|
+
allowed: false,
|
|
40
|
+
command: cmd,
|
|
41
|
+
reason: "Invoke command only allows read-only HTTP method GET.",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const invokeSecurityCheck = isInvokeAllowed({
|
|
46
|
+
area: area.toLowerCase(),
|
|
47
|
+
resource: resource.toLowerCase(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!invokeSecurityCheck.allowed) {
|
|
51
|
+
return {
|
|
52
|
+
allowed: false,
|
|
53
|
+
command: cmd,
|
|
54
|
+
reason: invokeSecurityCheck.reason,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { allowed: true, command: cmd };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The acr/account groups run against the Azure platform rather than Azure
|
|
62
|
+
// DevOps, so the platform credential rules apply to them here too. Without
|
|
63
|
+
// this, `acr credential show` passes the DevOps verb check and prints the
|
|
64
|
+
// registry's admin password. Matched across every word rather than
|
|
65
|
+
// positionally: stricter than az-tool's parse, which suits a legacy
|
|
66
|
+
// passthrough.
|
|
67
|
+
const standaloneGroup = commandWords[0];
|
|
68
|
+
if (
|
|
69
|
+
standaloneGroup &&
|
|
70
|
+
STANDALONE_AZ_COMMANDS.includes(standaloneGroup as (typeof STANDALONE_AZ_COMMANDS)[number])
|
|
71
|
+
) {
|
|
72
|
+
const credentialWord = commandWords.find(
|
|
73
|
+
(word) => isCredentialBlockedVerb(word) || isCredentialBlockedSegment(word),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (credentialWord) {
|
|
77
|
+
return {
|
|
78
|
+
allowed: false,
|
|
79
|
+
command: cmd,
|
|
80
|
+
reason: `'${credentialWord}' addresses credential material and is blocked.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const blockedWord = commandWords.find((word) =>
|
|
86
|
+
BLOCKED_SUBCOMMANDS.includes(word as (typeof BLOCKED_SUBCOMMANDS)[number]),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (blockedWord) {
|
|
90
|
+
return {
|
|
91
|
+
allowed: false,
|
|
92
|
+
command: cmd,
|
|
93
|
+
reason: `Command contains blocked operation '${blockedWord}'. Only read-only operations allowed: ${ALLOWED_SUBCOMMANDS.join(", ")}`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const allowedOperationIndex = commandWords.findIndex((word) =>
|
|
98
|
+
ALLOWED_SUBCOMMANDS.includes(word as (typeof ALLOWED_SUBCOMMANDS)[number]),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (allowedOperationIndex === -1) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
command: cmd,
|
|
105
|
+
reason: `Command must contain a read-only operation: ${ALLOWED_SUBCOMMANDS.join(", ")}. Example: "pipelines list", "repos show --id 123"`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// "run" is read-only only as `az pipelines run`. On the standalone groups it
|
|
110
|
+
// executes code in Azure (`az acr run`, `az acr task run`), so it is rejected
|
|
111
|
+
// anywhere other than the pipelines group.
|
|
112
|
+
if (commandWords[allowedOperationIndex] === "run" && commandWords[0] !== "pipelines") {
|
|
113
|
+
return {
|
|
114
|
+
allowed: false,
|
|
115
|
+
command: cmd,
|
|
116
|
+
reason: `'run' is only allowed as 'pipelines run'. Outside the pipelines group it executes commands in Azure.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { allowed: true, command: cmd };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isInvokeAllowed(params: InvokeParams): SecurityCheckResult {
|
|
124
|
+
const { area, resource } = params;
|
|
125
|
+
|
|
126
|
+
if (BLOCKED_INVOKE_AREAS.includes(area as (typeof BLOCKED_INVOKE_AREAS)[number])) {
|
|
127
|
+
return {
|
|
128
|
+
allowed: false,
|
|
129
|
+
reason: `Area '${area}' is blocked. Dangerous areas not allowed: ${BLOCKED_INVOKE_AREAS.join(", ")}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!ALLOWED_INVOKE_AREAS.includes(area as (typeof ALLOWED_INVOKE_AREAS)[number])) {
|
|
134
|
+
return {
|
|
135
|
+
allowed: false,
|
|
136
|
+
reason: `Area '${area}' is not in allowed list. Allowed areas: ${ALLOWED_INVOKE_AREAS.join(", ")}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const blockedResources = BLOCKED_INVOKE_RESOURCES[area];
|
|
141
|
+
if (blockedResources?.includes(resource)) {
|
|
142
|
+
return {
|
|
143
|
+
allowed: false,
|
|
144
|
+
reason: `Resource '${resource}' in area '${area}' is blocked. Write resources not allowed.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const allowedResources = ALLOWED_INVOKE_RESOURCES[area];
|
|
149
|
+
if (!allowedResources?.includes(resource)) {
|
|
150
|
+
return {
|
|
151
|
+
allowed: false,
|
|
152
|
+
reason: `Resource '${resource}' is not in allowed list for area '${area}'. Allowed resources: ${allowedResources?.join(", ") ?? "none"}`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { allowed: true };
|
|
157
|
+
}
|