Repository navigation
feat(atc): resolve finding check categories from the check variant form template #239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| /** | ||
| * Check Category Enrichment | ||
| * | ||
| * Resolves ATC findings to their check category (e.g., 'CI_PERFORMANCE', | ||
| * 'CI_SECURITY') through the check variant form template | ||
| * (GET /sap/bc/adt/atc/checkvariants/formtemplate?chkvName={name}). | ||
| * | ||
| * The form template is the check catalog of the run's check variant: every | ||
| * available check category lists its checks, and each check carries its | ||
| * implementing class. When a finding's checkId matches a catalog | ||
| * implementing class, the index resolves it directly. Live worklist | ||
| * checkIds are check-instance GUIDs (observed on a live S/4 system, | ||
| * 2026-10-22); those resolve through the documentation bridge in | ||
| * ./documentation-bridge.ts. | ||
| * | ||
| * Enrichment is best-effort by design: without the form template, or for a | ||
| * checkId outside the catalog, findings keep no category and the run still | ||
| * succeeds. | ||
| */ | ||
|
|
||
| /** | ||
| * Structural shape of the parsed check variant form template document | ||
| * (GET /sap/bc/adt/atc/checkvariants/formtemplate). | ||
| * | ||
| * Only the fields needed for the checkId -> category index are declared. | ||
| */ | ||
| export interface FormTemplateDocument { | ||
| formTemplate?: { | ||
| availableCheckCategories?: { | ||
| availableCheckCategory?: FormTemplateCategoryEntry[]; | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| /** One available check category with the checks it contains. */ | ||
| export interface FormTemplateCategoryEntry { | ||
| checkCategoryObject?: { | ||
| /** Category name (the enrichment key, e.g., 'CI_PERFORMANCE') */ | ||
| name?: string; | ||
| /** Category display title (e.g., 'Performance') */ | ||
| description?: string; | ||
| }; | ||
| check?: FormTemplateCheckEntry[]; | ||
| } | ||
|
|
||
| /** One check entry of a category. */ | ||
| export interface FormTemplateCheckEntry { | ||
| checkObject?: { | ||
| /** Check name (e.g., 'CI_ANALYZE_WHERE') */ | ||
| name?: string; | ||
| content?: { | ||
| /** The check's category assignment (the category name) */ | ||
| category?: { name?: string }; | ||
| /** | ||
| * Implementing class of the check (e.g., 'CL_CI_TEST_SELECT_TAW_A'). | ||
| * Worklist findings carry a check-instance GUID instead; the | ||
| * documentation bridge resolves that GUID to this class name. | ||
| */ | ||
| implementingClass?: { name?: string }; | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Build a checkId -> check category name index from a parsed check variant | ||
| * form template document. | ||
| * | ||
| * The checkId key is the implementing class name; the value is the category | ||
| * object name (a stable key like 'CI_PERFORMANCE', not the display title). | ||
| * | ||
| * @param doc - Parsed form template document (nullable for convenience) | ||
| * @returns Map of checkId -> category name; empty when the document is | ||
| * missing or malformed | ||
| */ | ||
| export function parseCheckCategoryIndex( | ||
| doc: FormTemplateDocument | null | undefined, | ||
| ): Map<string, string> { | ||
| const index = new Map<string, string>(); | ||
| const categories = | ||
| doc?.formTemplate?.availableCheckCategories?.availableCheckCategory ?? []; | ||
| for (const category of categories) { | ||
| collectCategoryChecks(category, index); | ||
| } | ||
| return index; | ||
| } | ||
|
|
||
| /** Index every check of one category under the category's name. */ | ||
| function collectCategoryChecks( | ||
| category: FormTemplateCategoryEntry, | ||
| index: Map<string, string>, | ||
| ): void { | ||
| const categoryName = category.checkCategoryObject?.name; | ||
| if (!categoryName) return; | ||
| const checkIds = (category.check ?? []) | ||
| .map((check) => check.checkObject?.content?.implementingClass?.name) | ||
| .filter((checkId): checkId is string => !!checkId); | ||
| for (const checkId of checkIds) { | ||
| index.set(checkId, categoryName); | ||
| } | ||
| } | ||
|
|
||
| /** Structural client surface needed to fetch a form template. */ | ||
| export interface FormTemplateClient { | ||
| adt: { | ||
| atc: { | ||
| checkvariants: { | ||
| /** GET /sap/bc/adt/atc/checkvariants/formtemplate{?chkvName} */ | ||
| formtemplate: (params?: { | ||
| chkvName?: string; | ||
| }) => Promise<FormTemplateDocument>; | ||
| }; | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Fetch the check variant form template and build the checkId -> category | ||
| * name index for a run. | ||
| * | ||
| * Best-effort: any error (network, 404, malformed document) is swallowed | ||
| * and reported through the optional logger so a failed enrichment never | ||
| * fails the ATC run itself. | ||
| * | ||
| * @param client - ADT client with the checkvariants.formtemplate surface | ||
| * @param checkVariant - The check variant name of the run (e.g., 'DEFAULT') | ||
| * @param logger - Optional CLI logger for diagnostics | ||
| * @returns checkId -> category name index; empty when unavailable | ||
| */ | ||
| export async function fetchCheckCategoryIndex( | ||
| client: FormTemplateClient, | ||
| checkVariant: string, | ||
| logger?: { | ||
| warn: (message: string) => void; | ||
| info?: (message: string) => void; | ||
| }, | ||
| ): Promise<Map<string, string>> { | ||
| try { | ||
| const doc = await client.adt.atc.checkvariants.formtemplate({ | ||
| chkvName: checkVariant, | ||
| }); | ||
| return parseCheckCategoryIndex(doc); | ||
| } catch (error) { | ||
| logger?.warn?.( | ||
| `⚠️ Check category enrichment unavailable: ${ | ||
| error instanceof Error ? error.message : String(error) | ||
| }`, | ||
| ); | ||
| return new Map(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Enrich findings with their check category in place. | ||
| * | ||
| * Generic over any finding shape carrying a checkId (and an optional | ||
| * checkCategory field), so both the CLI plugin's AtcFinding and other | ||
| * canonical finding types can be enriched with the same index. | ||
| * | ||
| * @param findings - Findings to enrich (mutated in place) | ||
| * @param index - checkId -> category name index | ||
| * @returns Number of findings that received a category | ||
| */ | ||
| export function enrichFindingsWithCategories< | ||
| T extends { checkId: string; checkCategory?: string }, | ||
| >(findings: T[], index: Map<string, string>): number { | ||
| let enriched = 0; | ||
| for (const finding of findings) { | ||
| const checkCategory = index.get(finding.checkId); | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| if (checkCategory) { | ||
| finding.checkCategory = checkCategory; | ||
| enriched++; | ||
| } | ||
| } | ||
| return enriched; | ||
| } | ||
|
|
||
| /** | ||
| * Enrich an ATC result with check categories in place. | ||
| * | ||
| * @param result - The run result (findings mutated in place) | ||
| * @param index - checkId -> category name index | ||
| * @returns Number of findings that received a category | ||
| */ | ||
| export function enrichResultWithCategories< | ||
| T extends { checkId: string; checkCategory?: string }, | ||
| >(result: { findings?: T[] }, index: Map<string, string>): number { | ||
| return enrichFindingsWithCategories(result.findings ?? [], index); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,11 @@ | |||||||||||||||||||
| import type { CliCommandPlugin, CliContext } from '@abapify/adt-plugin'; | ||||||||||||||||||||
| import chalk from 'chalk'; | ||||||||||||||||||||
| import { outputSarifReport, outputGitLabCodeQuality } from '../formatters'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| fetchCheckCategoryIndex, | ||||||||||||||||||||
| enrichResultWithCategories, | ||||||||||||||||||||
| } from '../check-categories'; | ||||||||||||||||||||
| import { resolveCategoriesViaDocumentation } from '../documentation-bridge'; | ||||||||||||||||||||
| import type { AtcResult, AtcFinding, OutputFormat } from '../types'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Client type - the plugin receives a typed client from context | ||||||||||||||||||||
|
|
@@ -34,6 +39,11 @@ interface AdtClient { | |||||||||||||||||||
| params: { includeExemptedFindings: string }, | ||||||||||||||||||||
| ) => Promise<WorklistResponse>; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| checkvariants: { | ||||||||||||||||||||
| formtemplate: (params?: { | ||||||||||||||||||||
| chkvName?: string; | ||||||||||||||||||||
| }) => Promise<import('../check-categories').FormTemplateDocument>; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| runs: { | ||||||||||||||||||||
| post: ( | ||||||||||||||||||||
| params: { worklistId: string }, | ||||||||||||||||||||
|
|
@@ -54,6 +64,15 @@ interface AdtClient { | |||||||||||||||||||
| }; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Bounded documentation text read (the real ADT client provides it). | ||||||||||||||||||||
| * Optional: without it the documentation category bridge is skipped. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| readTextBounded?: ( | ||||||||||||||||||||
| uri: string, | ||||||||||||||||||||
| maxBytes: number, | ||||||||||||||||||||
| options?: { headers?: Record<string, string> }, | ||||||||||||||||||||
| ) => Promise<string>; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Worklist response type | ||||||||||||||||||||
|
|
@@ -75,6 +94,8 @@ interface WorklistResponse { | |||||||||||||||||||
| checkTitle?: string; | ||||||||||||||||||||
| messageId?: string; | ||||||||||||||||||||
| messageTitle?: string; | ||||||||||||||||||||
| /** Documentation link: the page that names the check class. */ | ||||||||||||||||||||
| link?: Array<{ href?: string; rel?: string }>; | ||||||||||||||||||||
| }>; | ||||||||||||||||||||
| }; | ||||||||||||||||||||
| }>; | ||||||||||||||||||||
|
|
@@ -105,6 +126,25 @@ function adtLink(name: string, uri: string, systemName?: string): string { | |||||||||||||||||||
| return hyperlink(chalk.cyan(name), url); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * The trusted documentation link of one worklist finding. | ||||||||||||||||||||
| * | ||||||||||||||||||||
| * Live shape (2026-10-22): <atom:link rel="http://www.sap.com/adt/ | ||||||||||||||||||||
| * relations/documentation" href="/sap/bc/adt/documentation/atc/documents/ | ||||||||||||||||||||
| * itemid/{guid}/index/{n}"/> inside the atcfinding:finding element. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function documentationUriFromFinding( | ||||||||||||||||||||
| finding: NonNullable< | ||||||||||||||||||||
| NonNullable< | ||||||||||||||||||||
| NonNullable<WorklistResponse['worklist']>['objects']['object'] | ||||||||||||||||||||
| >[number]['findings']['finding'] | ||||||||||||||||||||
| >[number], | ||||||||||||||||||||
| ): string | undefined { | ||||||||||||||||||||
| return finding.link?.find( | ||||||||||||||||||||
| (link) => link.rel === 'http://www.sap.com/adt/relations/documentation', | ||||||||||||||||||||
| )?.href; | ||||||||||||||||||||
|
Comment on lines
+143
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Check whether readTextBounded accepts absolute URLs or only ADT-relative paths.
rg -nP -C8 '\breadTextBounded\b' packages/adt-client/src --type=tsRepository: abapify/adt-cli Length of output: 6914 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- PR diff: packages/adt-atc/src/commands/atc.ts ---'
git diff --unified=25 d69005e9effb5846717967e997821ecf8a39a650 0a6c75ce920182c5e7ae855bccb88a132f4c0fee -- packages/adt-atc/src/commands/atc.ts
printf '%s\n' '--- adapter bounded-read implementation and URL resolver ---'
sed -n '580,640p' packages/adt-client/src/adapter.ts
rg -n -F --glob '*.ts' -e 'function resolveAdtUrl' -e 'const resolveAdtUrl' -e 'resolveAdtUrl' packages/adt-client/src
printf '%s\n' '--- server documentation URI validation ---'
rg -n -F --glob '*.ts' -e 'ATC_DOCUMENTATION_URI' -e 'documentationUri' packages/adt-server/src/broker.ts
sed -n '1,100p' packages/adt-server/src/broker.ts
printf '%s\n' '--- ATC call sites and helper definitions ---'
rg -n -F --glob '*.ts' -e 'documentationUriFromFinding' -e 'resolveCategoriesViaDocumentation' -e 'readTextBounded' packages/adt-atc/srcRepository: abapify/adt-cli Length of output: 15730 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- adapter URL resolver and surrounding configuration ---'
sed -n '168,292p' packages/adt-client/src/adapter.ts
printf '%s\n' '--- documentation bridge ---'
sed -n '1,230p' packages/adt-atc/src/check-categories.ts
printf '%s\n' '--- server allow-list definition and use ---'
sed -n '220,255p' packages/adt-server/src/broker.ts
sed -n '1435,1465p' packages/adt-server/src/broker.tsRepository: abapify/adt-cli Length of output: 14421 Validate the documentation
🛡️ Suggested fix+const ATC_DOCUMENTATION_URI =
+ /^\/sap\/bc\/adt\/documentation\/atc\/documents\/itemid\/[A-Za-z0-9_-]+\/index\/\d+$/u;
+
...
- return finding.link?.find(
+ const href = finding.link?.find(
(link) => link.rel === 'http://www.sap.com/adt/relations/documentation',
- )?.href;
+ )?.href;
+ return href && ATC_DOCUMENTATION_URI.test(href) ? href : undefined;🤖 Prompt for AI Agents
Comment on lines
+143
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Restrict the worklist Prompt for AI agents
Suggested change
|
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Convert worklist response to AtcResult | ||||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
@@ -131,6 +171,7 @@ function convertWorklistToResult( | |||||||||||||||||||
| objectName: obj.name || '', | ||||||||||||||||||||
| location: finding.location, | ||||||||||||||||||||
| findingUri: finding.uri, | ||||||||||||||||||||
| documentationUri: documentationUriFromFinding(finding), | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
@@ -406,6 +447,38 @@ export const atcCommand: CliCommandPlugin = { | |||||||||||||||||||
| checkVariant, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Enrich findings with check categories from the run's check variant | ||||||||||||||||||||
| // form template (best-effort: never fails the run itself) | ||||||||||||||||||||
| const categoryIndex = await fetchCheckCategoryIndex( | ||||||||||||||||||||
| client, | ||||||||||||||||||||
| checkVariant, | ||||||||||||||||||||
|
Comment on lines
+452
to
+454
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium · Categories reflect edited variant When an administrator edits the selected check variant after the run finishes but before its form template is fetched, Bind enrichment to an immutable snapshot of the variant configuration used by the run. Also at Prompt for AI agentsIn `packages/adt-atc/src/commands/atc.ts` at lines 452-454:
**Categories reflect edited variant**
When an administrator edits the selected check variant after the run finishes but before its form template is fetched, `atcCommand`, `runAtc`, and MCP `atc_run` fetch the current catalog after retrieving findings, so the index can describe the edited configuration instead of the checks that produced those findings; callers receive incorrect categories.
Bind enrichment to an immutable snapshot of the variant configuration used by the run.
Also at `packages/adt-server/src/broker.ts:1434-1436`, `packages/adt-mcp/src/lib/tools/atc-run.ts:252-254`. |
||||||||||||||||||||
| ctx.logger, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| const enriched = enrichResultWithCategories(result, categoryIndex); | ||||||||||||||||||||
| if (enriched > 0) { | ||||||||||||||||||||
| ctx.logger.info( | ||||||||||||||||||||
| `🏷️ Enriched ${enriched}/${result.totalFindings} finding(s) with check categories`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Bridge remaining findings through their ATC documentation page: the | ||||||||||||||||||||
| // worklist checkId is a check-instance GUID, not the implementing class | ||||||||||||||||||||
| // the form-template catalog lists (live evidence, 2026-10-22), so | ||||||||||||||||||||
| // the direct index match cannot resolve them. The documentation page | ||||||||||||||||||||
| // names the check class, which resolves through the same index. | ||||||||||||||||||||
| // Best-effort: never fails the run. | ||||||||||||||||||||
| const bridged = await resolveCategoriesViaDocumentation( | ||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The documentation bridge makes one serial ADT read per distinct documentation URI, with no cap on how many. A run with many findings adds that many sequential requests, which conflicts with the 'bounded' lookup claim in the PR. Cap the number of documentation reads, or run them with limited concurrency, inside Prompt for AI agents |
||||||||||||||||||||
| client, | ||||||||||||||||||||
| result.findings, | ||||||||||||||||||||
| categoryIndex, | ||||||||||||||||||||
| { logger: ctx.logger }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| if (bridged > 0) { | ||||||||||||||||||||
| ctx.logger.info( | ||||||||||||||||||||
| `🏷 Bridged ${bridged} finding(s) to check categories via ATC documentation`, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Load finding resolver if requested | ||||||||||||||||||||
| let resolver: import('../types').FindingResolver | undefined; | ||||||||||||||||||||
| if (options.resolver) { | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Worklist findings carry a GUID in
checkId, not the implementing class, so this describes the resolution incorrectly;enrichResultWithCategoriesonly performs a direct ID lookup and will not enrich normal findings. Document thedocumentationUri-to-class bridge and distinguish it from the direct helper.Prompt for AI agents