assessment-report-converter
Convert an arbitrary CSV report (e.g. a Black Duck export or a custom migration-issue inventory) into a schema-valid assessment `report.json` the modernization pipeline can consume — so it appears in the assessment UI and migration solutions resolve automatically. This skill is LLM-driven: you read and interpret the CSV yourself and author `report.json` by hand against the schema. A small helper script only does deterministic lookups (migration solutions, the ruleId for a solution) and validates the finished report. There is NO "convert everything" script and NO assumed column layout. Triggers
Assessment Report Converter (CSV → report.json)
What this skill does
You take a row-oriented CSV of migration issues and produce a canonical report.json conforming to the
assessment report schema. Once written to the versioned reports directory, the
report is picked up by the assessment UI and by the solution-resolution logic — exactly like a native AppCAT report.
This is an LLM-driven conversion, not a fixed mapping script. CSV layouts vary between tools and change over time,
so you read the file, decide what each column means, classify every row, and author report.json. You do not
rely on hard-coded column names.
A small helper script — scripts/report_tools.sh (bash + jq) or its PowerShell twin scripts/report_tools.ps1 (PowerShell 7+) — provides only the deterministic pieces you should never guess:
list-solutions— discover the available migration solutions.rules-for-solution— get the canonicalruleId(s) for a chosen solution.upgrade-solutions— the canonical JDK / Spring Boot / Spring Framework / Jakarta EE upgrade solutions + ruleIds.validate— structural + consistency validation of the finishedreport.json.
flowchart LR
A[Read CSV] --> B[Understand columns] --> C[Find projects] --> D{Audit each row}
D -->|CVE / CWE| E[Security finding]
D -->|Needs upgrade| F[Upgrade incident]
D -->|Other| G[Solution → ruleId → incident]
E --> H[Assemble report]
F --> H
G --> H
H --> I[Validate] --> J[Save report.json] --> K[Summarize]
Input parameters
csv-path(mandatory): Path to the source CSV file.workspace-path(optional): Output root. Defaults to the current directory. The report is written to{workspace-path}/.github/modernize/reports/report-{reportId}/report.json.producer(optional): A label identifying the source tool, stored inreport.producer(e.g."Black Duck"). Defaults to"CSV import".
When to use this skill
Use this when you have a CSV — not an AppCAT report.json — and you want it to behave like a real assessment
report: a Black Duck / third-party export, or a hand-maintained spreadsheet of migration issues. Do not use it to
run analysis from source code (that is the assessment skill).
The helper script
The helper ships as two interchangeable implementations of the same CLI — use whichever fits the machine:
scripts/report_tools.sh (bash + jq) and
scripts/report_tools.ps1 (PowerShell 7+, no extra dependencies). Both behave
identically and never read the CSV. Run them from the scripts/ directory.
# bash + jq
./report_tools.sh list-solutions [--query KW] [--type Formula|Chat] [--ids-only]
./report_tools.sh rules-for-solution <solutionId>
./report_tools.sh upgrade-solutions
./report_tools.sh validate <path/to/report.json>
# PowerShell 7+
pwsh ./report_tools.ps1 list-solutions [--query KW] [--type Formula|Chat] [--ids-only]
pwsh ./report_tools.ps1 rules-for-solution <solutionId>
pwsh ./report_tools.ps1 upgrade-solutions
pwsh ./report_tools.ps1 validate <path/to/report.json>
list-solutionsprints the matching solutions from scripts/solution-mapping.json as whole JSON objects (each always hassolutionId,name,type, andtooltip; some also carryeffort/prompt);--queryfilters by substring on id/name/tooltip.rules-for-solutionprints{ solutionId, ruleCount, rules: [{ruleId, sourceCategory}], preferredRuleId }. An empty list means the solution has no rule (e.g. a security-only solution) — do not invent a ruleId.upgrade-solutionsprints, per component (jdk,spring-boot,spring-framework,jakarta-ee), thesolutionIdand its resolvedpreferredRuleId.validateruns structural + cross-field consistency checks (incident→rule references, requireddomain/categoryrule fields, enum values, security-finding shape and dedupe, andmetadata.domains↔content consistency). Exit code is0valid,1invalid,2when the report can't be read/parsed. It accepts a--schema <path>flag for CLI compatibility but ignores it — the checks are self-contained.
Reference material
Detailed reference material is consolidated in the Reference section at the end of this file. The workflow below links to the relevant part at the step where you need it — you do not have to open any separate files:
- Report structure — the top-level
report.jsonshape and the authoring conventions the schema can't express (the schema owns the field shape). - Assessment domains — the
metadata.domainsvalues and when to include each. - Security severity mapping — normalizing a source CVE/CWE severity to the security
mandatory|potential|optionalscale. - Rule classification — the required
domain/categoryrule fields that group and render a rule's incidents.
Workflow
Step 1 — Understand the report structure (do this first)
Read the authoritative schema scripts/assessment-report.schema.json — it is
the source of truth for the field shape. The report is a single root object { version, producer, metadata, projects, rules, security? }, and additionalProperties is false almost everywhere (there is no summary object). The
report structure reference adds the authoring conventions the schema can't express.
You must know the required fields before authoring anything.
Step 2 — Read and understand the CSV
Read the CSV file directly (headers + a representative sample of rows). Then interpret the columns by meaning, since names and order vary by tool. Identify whichever of these the CSV actually carries (any may be absent):
- a project / module / service identifier,
- an application name,
- an issue title and description,
- a severity / criticality / priority,
- a category / domain / type,
- a CVE / CWE identifier,
- an affected component / package / library and its version,
- a file path and line,
- an effort / story-point estimate,
- a reference / URL.
When a concept is missing, leave the corresponding report field empty or at its default — never fabricate data.
Step 3 — Determine the projects
Work out how many projects the CSV describes and group rows accordingly:
- If a project/module/service column exists, group rows by it — one project per distinct value, using that value
as
project.path. - If the CSV has no project information, create a single project with
path"."and minimal properties (appName""; leave optionaljdkVersion/frameworks/languages/toolsoff or empty). - Populate
project.propertiesonly from what the CSV actually provides; otherwise keep them empty. OnlyappNameis required.
Every incident you create later belongs to exactly one of these projects.
Step 4 — Audit each row, within its project
Classify every row into exactly one of three branches and attach the result to the row's project. Every rule you add to
rules{} must set its domain and category fields (that is what groups and renders its incidents) — see
rule classification. Incidents do not require any label.
-
CVE / CWE → security finding (directly). When a row carries a
CVE-…/CWE-…identifier (or unambiguously describes one), add a security finding toreport.security[]. Capture as much of the column as possible:id(the CVE/CWE token),title,category,severity(the security scalemandatory | potential | optional— normalize the source severity per security issue severity mapping),description,evidence.files(affected paths),evidence.explanation, and optionalstoryPoint. Merge by id — one finding per CVE/CWE; accumulate evidence files and keep the strongest severity (mandatory>potential>optional). Security findings are not incidents; they live only inreport.security[](there is no summary to count them in). -
Needs a major-component version upgrade → upgrade incident. If the row implies upgrading a major component — JDK, Spring Boot, Spring Framework, or Java EE / Jakarta EE (e.g. a CVE against
spring-boot, an out-of-support runtime, an explicit "upgrade JDK") — run theupgrade-solutionscommand, pick the component, and use itspreferredRuleId. Add that rule torules{}(severity: "mandatory",domain: "java-upgrade", acategory, a reasonableeffort) and add an incident to the project — one upgrade rule per component per project, one incident per triggering row. A CVE that implies an upgrade produces both a security finding (branch 1) and an upgrade incident — that is what makes the upgrade resolve as a migration solution. -
Any other issue → solution → ruleId → incident. Find the migration solution that fits the issue with the
list-solutions --query <keyword>command, then get its canonical ruleId withrules-for-solution <solutionId>(usepreferredRuleId). Add that rule torules{}(withdomain,category,severity,effort) and an incident to the project. If no solution fits, you may still record the issue with a clear synthetic ruleId (it just won't carry an automatic Formula solution) — or leave it for the "remaining" list in your summary. Either way, report it.
Process rows in parallel
Classifying a row is independent work, so for large CSVs do it concurrently rather than one row at a time:
- Batch the rows (e.g. 20–50 per batch, or one batch per project) and dispatch the batches in parallel — launch
several
Explore/worker subagents at once, each auditing its batch into a partial result (security findings, upgrade hits, and ordinary incidents with their resolved ruleIds). Ask each worker to return structured JSON; it does not write files. - Share the deterministic lookups. Run the
upgrade-solutionscommand and thelist-solutions/rules-for-solutionqueries once up front (results are stable) and pass them to the workers, so parallel batches don't repeat the same lookups or race on them. - Keep workers side-effect free, then merge sequentially in one place so shared state stays correct:
- Security findings — merge by
id(one finding per CVE/CWE; unionevidence.files, keep the strongest severity). - Upgrade rules — collapse to one rule per
(project, component); keep every triggering incident. incidentIds — assign"<ruleId>/<n>"after the merge, never inside a worker (so ids are deterministic regardless of batch order).
- Security findings — merge by
- If the CSV is small, just process the rows sequentially — the parallel split only pays off at scale.
Step 5 — Assemble the report and finalize metadata
There is no summary object to compute — assemble the top-level document and fill metadata:
projects[]= your projects, each withproperties(onlyappNamerequired) and itsincidents[].rules{}= every distinct rule you referenced, keyed by ruleId, each withid,title,severity,effort,domain, andcategory.report.security[]= the deduped findings (omit or leave empty when there are none).metadata.domains= the assessment domains your report actually has content for, consistent with yourrule.domainvalues — see assessment domains.metadata.mode(optional) ="full"when any security finding exists, else"issue-only".metadata.status="completed";metadata.targetIds= the target ids in scope (may be empty).metadata.id(and the report-directory id) =analysisStartTimeformattedyyyyMMddHHmmss(UTC); use the current UTC time when the CSV has no timestamp.
Step 6 — Write and validate
Write report.json to the versioned location (Output location), then validate and fix until clean:
# bash + jq
./report_tools.sh validate "<workspace>/.github/modernize/reports/report-<id>/report.json"
# …or PowerShell 7+
pwsh ./report_tools.ps1 validate "<workspace>/.github/modernize/reports/report-<id>/report.json"
Resolve every reported consistency error before finishing.
Step 7 — Summarize for the user
Report a concise conversion summary:
- Converted rows — number of security findings (CVE/CWE), upgrade incidents (with components), and ordinary issue incidents (with the solutions they mapped to); plus the project / rule / incident counts and the report path + id.
- Remaining rows — rows you could not confidently map, and why (no matching solution, ambiguous column, missing id).
- Suggestions — concrete next steps (e.g. pick a specific solution for a remaining row, add a project column to the CSV, supply severities), so the user can close the gaps.
Output location
{workspace-path}/.github/modernize/reports/report-{reportId}/report.jsonreportId= the report'smetadata.idfrom Step 5 (analysisStartTimeasyyyyMMddHHmmss, UTC).- Consider copying the original CSV next to
report.jsonassource.csvfor provenance.
Success criteria
- ✅
report.jsonis written to the versioned reports directory and thevalidatecommand reports VALID. - ✅ Projects reflect the CSV (one per module/service, or a single project with
appName: ""when none is given). - ✅ CVE/CWE rows are
security[]findings (deduped by id) with amandatory|potential|optionalseverity;modeisfull. - ✅ Rows implying a JDK / Spring Boot / Spring Framework / Jakarta EE upgrade add a mandatory upgrade incident whose
ruleIdcame from theupgrade-solutionscommand. - ✅ Other issues map to a solution's canonical
ruleId(vialist-solutions+rules-for-solution) wherever one fits. - ✅ Every rule carries
domainandcategoryfields; every incident'sruleIdresolves to a rule inrules{}; CVE/CWE findings stay insecurity[], notincidents[]. - ✅
metadata.domainsmatches the report's content and the emittedrule.domainvalues (enforced byvalidate). - ✅ The user gets a summary of converted rows, remaining rows, and suggestions.
Troubleshooting
validatereports a consistency error (e.g. an incidentruleIdwith no matching rule, or adomains/securitymismatch) — the message names the exact field; fix that field.- Don't add fields the schema doesn't allow (e.g. a
summaryobject, orissues/storyPointson a project). The helper'svalidateis lenient about extra keys, but the app's importer enforcesadditionalProperties: falseand will reject the report — this schema has no summary. Keep to the documented shape. - A row has a CVE/CWE and needs an upgrade — emit both: a
security[]finding and an upgrade incident. They are not duplicates; the finding documents the vulnerability, the incident drives the upgrade solution. - No solution fits an issue —
list-solutions --queryreturns nothing useful. Record the issue with a synthetic, descriptiveruleId(no Formula will attach) or list it under "remaining" with a suggestion. rules-for-solutionreturns an empty list — that solution has no rule (often security-only). Don't fabricate a ruleId; handle the issue via the security-finding path or pick a different solution.- Wrong severity enum — rule/incident severity is the 4-value
mandatory|potential|optional|informationenum; security-finding severity uses the same scale minusinformation(mandatory|potential|optional, since a finding is always at least optional). Don't use the oldcritical|high|medium|low|infovalues.
Reference
Consolidated reference material. The workflow above links here at the step where each part is needed.
Report structure
You author a single report.json conforming to the authoritative schema
scripts/assessment-report.schema.json. The schema is one root object
(draft-07, its sub-types live under definitions) with the top-level shape { version, producer, metadata, projects, rules, security? }. Read the schema for the exact required fields, types, and enums per object — it is the source of
truth and the validate command checks it, so this section does not restate the field list. additionalProperties
is false almost everywhere, so do not invent fields (there is no summary object, and projects have no issues /
storyPoints).
Authoring rules of thumb — the conventions and cross-field rules the schema can't fully express:
versionis the string"1.0.0"(the schema accepts any string; this is the value to use).- Rule/incident
severityis the enummandatory | potential | optional | information. Map source severities by meaning (e.g. critical/blocker →mandatory, major/medium →potential, minor/low →optional, info →information). - Classification is done with rule fields, not labels. Each
rules{}entry setsdomain(cloud-readiness | java-upgrade | security) andcategory(a free string heading) directly — see Rule classification.rules{}.labelsandincidents[].labelsare optional free-form arrays; you normally leave them out. - Security-finding
severityuses the report criticality scalemandatory | potential | optional(the ruleSeverityvalues minusinformation— a security finding is always at least optional) — normalize per Security severity mapping. metadata.domainsrecords which assessment domains the report has content for — see Assessment domains. It must be consistent with therule.domainvalues you emit.incidentIdconvention:"<ruleId>/<n>"(n is a per-rule counter).locationKind="source-file"when a file path is present, else"unknown".statusis normally"completed".metadata.modeis optional; use"full"when the report has security findings, else"issue-only".
Assessment domains
metadata.domains is a string[] recording which assessment domains produced the report. Allowed values are
cloud-readiness, java-upgrade, and security.
| Domain | Meaning | When to include it for a CSV conversion |
|---|---|---|
cloud-readiness | Azure cloud-migration issues | any ordinary issue → solution incident (branch 3) |
java-upgrade | JDK / Spring Boot / Spring Framework / Jakarta EE upgrades | any upgrade incident (branch 2) |
security | CVE / CWE vulnerability findings | report.security[] is non-empty (branch 1) |
- The native Java default is
["cloud-readiness", "java-upgrade"]. - Set
metadata.domainsto exactly the domains your report has content for — don't listsecuritywith no findings, orjava-upgradewith no upgrade incidents.validateflags asecurity/report.securitymismatch in either direction. metadata.domainsmust be consistent with therule.domainfield on your rules — everyrule.domainvalue you emit should appear inmetadata.domains.
Security severity mapping
A security finding's severity uses the report criticality scale — mandatory | potential | optional. This is the rule
Severity enum minus information (a security finding is always at least optional). The extension renders these
values directly, so there is no separate security severity scale and no conversion step.
Map the source CVE/CWE severity by meaning to the nearest value (case-insensitive):
| Source severity (CSV, case-insensitive) | Report security[].severity |
|---|---|
critical / blocker | mandatory |
high | mandatory |
medium / moderate | potential |
low | optional |
| anything else / unknown / missing | optional |
- When merging duplicate findings by
id, keep the strongest severity (mandatory>potential>optional). - Security findings live in
report.security[]only; there is nosummaryobject to key by severity.validatechecks each finding'sseverityis one of the three values and that findings are unique byid. - Include the
securitydomain inmetadata.domainswheneverreport.security[]is non-empty (and only then).
Rule classification
In this schema a rule is classified with fields on the rule object, not with labels. Every rules{} entry is
required to carry a domain and a category (the validate command enforces both, and the schema rejects a
rule that is missing them).
| Field | Value | Effect |
|---|---|---|
domain | cloud-readiness | java-upgrade | security | Groups the rule under that domain tab. Use the same domain you list in metadata.domains. Required. |
category | the issue category heading (e.g. postgresql, java-version-upgrade, deprecated-apis) | Shown as the group heading. Required. |
Choosing category (and the source label). Each mapped rule also has a sourceCategory in the solution mapping,
which you get from rules-for-solution <solutionId> (rules[].sourceCategory). The UI builds the group heading as
category when it equals sourceCategory (or sourceCategory is empty / null), otherwise as
category (sourceCategory). So:
- When a rule's
sourceCategoryis non-null, set itscategoryto exactly that value — e.g.mi-postgresql→ ruleazure-database-postgresql-02000,sourceCategory: "postgresql"→category: "postgresql", which renders as one clean Postgresql heading (mismatching it, e.g.category: "database", would render the doubled Database (Postgresql)). - When
sourceCategoryisnull(e.g. the JDK-upgrade rules), choose a sensiblecategoryyourself (e.g.upgrade) — it renders as-is.
Labels are optional. rules{}.labels and incidents[].labels are optional free-form string[]s in the schema.
Classification no longer depends on them, so you normally leave them out. The engine may still emit context labels
(target=, os=, capability=) on native reports, but when hand-authoring a CSV conversion you do not need any label
to make content render — the required domain/category fields do that.
microsoft/github-copilot-modernization · MIT · Revision 5d2a3268d816
Be the first to comment
Share what worked or leave a question for the creator.