Skip to content

GUI Attestation & Comment Engine — supersedes #8278#8283

Draft
aaronlippold wants to merge 64 commits into
masterfrom
feature/attestation-comment-engine
Draft

GUI Attestation & Comment Engine — supersedes #8278#8283
aaronlippold wants to merge 64 commits into
masterfrom
feature/attestation-comment-engine

Conversation

@aaronlippold

@aaronlippold aaronlippold commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

GUI attestation and comment engine for Heimdall — enabling compliance reviewers to attest Not Reviewed controls, add comments to any control, and export/import attestation files compatible with SAF CLI.

Supersedes PR #8278 (codex/editable-comments) which was architecturally wrong (buried editing in Details tab, triple-write mutation, CKL format logic in Vuex store, zero tests).

Builds on prior art from checklistView branch — adapts the proven form widgets (ChecklistRuleEdit.vue severity override + status dropdown, ChecklistSeverityOverride.vue justification-gate pattern) while discarding the CKL-only data model and direct-mutation antipattern. The checklistView branch was never merged due to scope creep (54 files, 3,881 lines), broken filtering, and CKL-only typing. This PR takes the good UX patterns and rebuilds with a format-agnostic architecture that works for InSpec, Nessus, ZAP, Burp, and every other HDF format — not just STIGs.

Design document: docs/adr-001-attestation-comment-engine.md — comprehensive ADR covering business workflows, UX design, data architecture, code inventory, and 3-phase implementation plan. Reviewed by 11 independent review agents across 3 rounds.

What this replaces

The CKL-email-STIG-Viewer loop where reviewers email CKL files back and forth, lose track of versions, and have no audit trail. Heimdall becomes the single source of truth with:

  • Attest — change NR control status to Passed/Failed/NA with explanation + TTL
  • Comment — annotate any control with reviewer notes (append-only log with who/when)
  • Export attestations — SAF CLI compatible JSON/YAML for saf attest apply
  • Import attestations — load previously exported attestation files
  • Side panel UX — right-side review panel with prev/next navigation, not buried in accordion tabs
  • Severity override — adapted from checklistView branch's ChecklistSeverityOverride.vue pattern
  • Finding details — assessor evidence field, maps to CKL FINDING_DETAILS

Completed so far

Foundation (committed)

  • setControlDescription + syncChecklistVulnComments + sanitizeCklSectionMarkers in hdf-converters (21 tests)
  • Generic updateControlDescription mutation + dirtyFileIds tracking in data_store (6 tests)
  • beforeunload guard + SidebarFileList dirty indicator + ExportJson markFileSaved wiring

Codebase cleanup (committed)

  • Fixed isolatedModules type export errors across inspecjs + 59 frontend files
  • Silenced sass deprecation warnings (2105 → 0 code warnings)
  • Centralized HeimdallToolsVersion in shared helper (DRY: 31 files → 1 import)
  • AppConfig singleton + NestJS Logger (replaced console.log)
  • Dev server progress spam disabled
  • Backend console.log → NestJS Logger audit

Design (committed)

  • ADR-001: full architecture decision record with glossary, UX mockups, data model, code inventory, approval flow design, prior art analysis (checklistView branch), and phase breakdown

Remaining work

Phase 1 — Core Attest/Comment Engine (~42 sp, ~2 hrs Claude-pace)

Implementation order: store → export/import → UI → integration. Each layer fully testable before the next starts. Store works without UI. Export works without side panel. Side panel consumes what's already tested.
12 cards covering: annotation Vuex store, review side panel (forms + history + a11y), control row actions, notification bar, NR status card action, dirty tracking wiring, CKL export sync, attestation file export/import, integration tests.

Phase 2 — Server Persistence + Collaboration (~38 sp, ~2 hrs)

7 cards: DB schema, API endpoints, save-as-revision, Draft→InReview→Final state machine, changelog sidebar, revision diff, CKL round-trip detection.

Phase 3 — Enterprise Workflows (~39 sp, ~2.25 hrs)

6 cards: approval workflows, POA&M/eMASS export, bulk attestation, expiration dashboard, shared edit notification, false positive/risk acceptance attestation.

Key architectural decisions

Decision Choice Why
Review UI Right-side panel (persistent on lg+) Results stay visible, prev/next navigation, matches STIG Viewer workflow
Data integrity Separate annotation records + display mutations Audit trail preserved, original scan reconstructable, XSS-safe
Format approach HDF-first, CKL as a layer on top Works for all scanner formats, not just STIGs. Borrows UX from checklistView but discards CKL-only data model
Attestation format SAF CLI 6-field format Interop with saf attest apply toolchain
Comment model Append-only log Every reviewer's comment preserved with who/when
CKL sync Export-time only No per-keystroke cost, format knowledge in hdf-converters
Phase 1 persistence Vuex + exportable files Validate UX before building DB schema
Prior art Adopt checklistView widgets, discard data model Severity override flow, status dropdown, form layout patterns reused. Direct mutation + CKL-only typing discarded.

Test plan

  • Unit tests: annotation store, ReviewPanel, ControlRowHeader actions, AnnotationBar, export/import formats
  • Vitest: all existing 34+ frontend tests pass, 21 hdf-converters tests pass
  • Build: npx vue-cli-service build with 0 code warnings
  • Playwright: side panel open/close, attest NR control, add comment, export, import, dark mode
  • E2e: Cypress database CRUD tests pass
  • Manual: load sample → attest → export HDF → verify attestation_data in output
  • Manual: load sample → comment → export CKL → verify COMMENTS section in output
  • Manual: export attestation file → import into fresh session → verify records applied


Build a GUI attestation and comment engine that:

1. Makes Heimdall Server the **single source of truth** for compliance review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not going to be the single source of truth. The downstream GRC tool (eMASS, etc) is the ultimate source of truth. Heimdall is for aggregating and normalizing data and producing reports for downstream and giving people a day-to-day view of their posture. Telling ISSOs that we're letting them do full reviews in Heimdall will require a lot more features than just attestation and commenting -- POAMs, risk adjustments etc. like we were building into HDF Libs as amendments.

We should not try to add all of those things here because at that point we'd be actually doing the bedrock-level schema update. But I also think this PR in general is trying to add tons of rigor to the comment process because it's trying to make it a compliance tool.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. §8.1 now reads: "Heimdall becomes the day-to-day posture view and review collaboration tool for compliance teams (downstream GRC tools like eMASS remain the source of truth for formal authorization)." §2 and §3.2 already incorporated your feedback about keeping Phase 1 simple — timestamps + identity, no RBAC. Commit 2b7770adb.

- `updated_by`: current user
- `control_id`: from the control being annotated

**Effect on display (immediate, in-memory):**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I've been trying to figure out from reading this is just where the user can read prior comments in the Heimdall UI. From context later I guess it's a pullout drawer. Can a user reply to another comment (if no, I am NOT suggesting we do that in this PR, because that was it's own large amount of effort when we did that in Vulcan)? Do other users see displayed who said what and when (I know it's in the data layer but is it in the presentation layer?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in §4.2 — Annotations Tab inside control expansion shows comment history (newest-first) with who/when. No reply threading in Phase 1. Component: AnnotationsTab.vue.


**1. Failed → Not Applicable / Risk Acceptance**

ISSOs routinely mark failed controls as Not Applicable (system doesn't have the component) or risk-accepted (finding is real but accepted with mitigation). This is NOT an edge case. The current `attestationCanBeAdded()` restricts attestation to `skipped` (NR) controls only. Phase 1 should at minimum support Failed→NA by extending the status check in the side panel UI (the store can accept any status; the restriction is in the validator). Full risk-acceptance with POA&M fields is Phase 3.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later it says this PR is just supposed to be phase 1.

I'd think that if we do phase 3 -- full on POAM support - then we're pretty much rebuilding a bunch of the new HDF libs right back in the heimdall2 monorepo where we were specifically trying not to put it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. §7.3 now explicitly states Phase 3 POA&M should consume the updated HDF Libs rather than reimplementing in the Heimdall monorepo. Card S1 is a wrapper around HDF Libs, not new domain logic.

- Available for ALL controls, not just NR
- Maps to CKL `COMMENTS ::` section in structured comments

### 3.1.3 Domain Gaps to Address

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated libraries in HDF Libs allow for doing a lot of these gaps (via CLI). We need to be careful we are not duplicating work just to backport it to the old schema.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted in §3.1.3 — we reference HDF Libs for these gaps. Phase 1 builds only the GUI attestation/comment workflow that HDF Libs doesn't cover (interactive review in the browser).


### 3.4 Export Artifacts

Two distinct export types:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably want a third -- export the original HDF file from before it was attested

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as §3.4.2 'Export Original (un-attested HDF)'. Card 9go.53 created.

│ ┌────────┬──────────────────────┬───────┬──────┬───────────────────┐ │
│ │Status │ Title │ ID │ NIST │ Actions │ │
│ ├────────┼──────────────────────┼───────┼──────┼───────────────────┤ │
│ │NR ▾ │ htpasswd files... │V-2255 │ AC-3 │ [Attest 📝] │ │

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this is written makes me think that you are saying you can ONLY do an attestation on an NR control and you can't just comment on it. You should have a comment button on all controls, and also an attestation button on the NR status ones.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified in §4.2 — comment section renders for ALL controls. Attest section renders only for NR controls. Both are in the Annotations Tab.

└──────────────────────────────────────────────────────────────────────────┘
```

### 4.2 Review Side Panel

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to move this into the tabs in the individual controls. So you'd expand out V-123456 and se:

| Test | Details | Code | Annotations (N) |

Just my two cents. That merges this feature into the existing UI in a reasonably intuitive way instead of adding a whole new drawer that is even noted in this doc to not really be used before elsewhere.

From the Annotations tab you can leave a comment or create an attestation in pretty much the same way you have it laid out in the drawer. We might even keep buttons for "Comment" and "attest" in the collapsed row for easy access; when clicked they'd just expand the row and focus the Annotations tab.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adopted. §4.2 now uses Annotations Tab inside control expansion: | Test | Details | Code | Annotations (N) |. Side panel/drawer design replaced. §8.3 tradeoff table updated. Card 9go.54 rewritten for this UX.

All options: save file (no dirty state change — exports the overlay, not the results)
```

### 5.5 How Import Works

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you have multiple HDF files loaded in the dashboard when you load an attestation? Do we try to apply the attestation to each of them? Just one?

E.g. what if we scan two windows servers and upload an attestation file that's only supposed to be applied to one of them?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in §3.5 — file selection modal when multiple HDF files are loaded. User picks which file(s) to apply attestations to, with control match counts displayed.


**State machine:**
```
DRAFT ──save──▸ IN REVIEW ──mark final──▸ FINAL

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I'm talking about when I say that the idea that Heimdall is now the "single source of truth" for compliance data leads to very heavy complexity.

We could implement attestations and comments that have timestamps and identity in them that still do not require us to add a workflow to review and finalize the whole file from edits. So the user still has the basic auditability of "who said what and when" about a control but we don't have to implement a ton of RBAC and workflows to have an ISSO formally bless it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and updated. §8.1 changed from 'single source of truth' to 'day-to-day posture view and review collaboration tool.' §3.2 simplified: Phase 1 has no formal state machine — just timestamps + identity. No RBAC, no approval gates, no finalization workflow.

@aaronlippold aaronlippold had a problem deploying to heimdall2-feature-attes-fdeulf June 23, 2026 01:15 Failure
aaronlippold added a commit that referenced this pull request Jun 23, 2026
Addresses 7 review comments on PR #8283:
1. Heimdall is day-to-day posture view, not GRC source of truth (eMASS is)
2. Phase 3 POA&M should wrap HDF Libs, not reimplement — follow-on PR
3. Add "export original un-attested HDF" as third export option
4. Comment button on ALL controls, attest button on NR only (clarified)
5. Annotations tab inside control expansion replaces side panel drawer
6. Attestation import: file-selection modal when multiple HDF files loaded
7. Drop Draft→In Review→Final state machine from Phase 1 — timestamps +
   identity provide auditability without RBAC/workflow complexity

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold aaronlippold had a problem deploying to heimdall2-feature-attes-fdeulf June 23, 2026 02:28 Failure
…f-converters

Add setControlDescription (write-side complement to getDescription),
sanitizeCklSectionMarkers (prevents CKL section injection on round-trip),
and syncChecklistVulnComments (export-time passthrough sync using existing
ChecklistVuln types). 21 tests covering array-form, object-form, structured
comment preservation, marker escaping, and multi-field edits.

Part of epic heimdall2-9go: GUI Attestation & Description Editing Engine.
Supersedes the ad-hoc CKL parsing in PR #8278.

Authored by: Aaron Lippold<lippold@gmail.com>
…ion warnings

- Split type-only re-exports from value re-exports in inspecjs index files
  (TS1205: export type required with isolatedModules)
- Add import type for ContextualizedControl and HDFControlSegment in Vue
  components (TS1272: decorated signature requires import type)
- Configure silenceDeprecations in vue.config.js sassOptions for known
  Vuetify/sass deprecations (import, global-builtin, slash-div, etc.)
  Reduces build warnings from 2105 to 25.

Authored by: Aaron Lippold<lippold@gmail.com>
…odules)

Change import { TypeName } to import type { TypeName } for all type-only
imports (interfaces, type aliases) across ~50 frontend files. Split mixed
imports that combine types and values into separate statements.

Fixes all webpack "export not found" warnings caused by isolatedModules
stripping type-only re-exports from barrel files. Reduces frontend build
warnings from 25 to 0 (code warnings; asset size advisories remain).

Authored by: Aaron Lippold<lippold@gmail.com>
Replace 31 individual import {version} from '../package.json' statements
with a single HeimdallToolsVersion export from utils/global.ts. Fixes
webpack "named export from default-exporting module" warnings and
eliminates the DRY violation of every mapper reading package.json
independently.

Authored by: Aaron Lippold<lippold@gmail.com>
…s spam

Configure silenceDeprecations in vue.config.js sassOptions for Vuetify 2 /
Bootstrap 4 deprecations (import, global-builtin, slash-div, legacy-js-api,
color-functions, if-function). Extracted to shared const for DRY.

Set devServer.client.progress: false to suppress webpack progress-plugin
percentage and "Build finished" output during development.

Authored by: Aaron Lippold<lippold@gmail.com>
Convert AppConfig to proper singleton pattern (static getInstance() with
private constructor). Replace all new AppConfig() calls with
AppConfig.getInstance() in config.service.ts, database.ts, groups.service.ts.

Replace console.log with NestJS Logger in app_config.ts and
token.providers.ts. Remove noisy .env-read logs from seeder (keep only
console.warn for missing .env, which is correct for Sequelize CLI context).

Remove express:* from DEBUG env var to eliminate express router debug noise.

Authored by: Aaron Lippold<lippold@gmail.com>
…ata_store

Add updateControlDescription mutation that calls setControlDescription from
hdf-converters and patches control.hdf.descriptions cache. Zero CKL/format
knowledge in the store.

Add dirty file tracking via FileID[] array (Vue 2 reactive pattern matching
data_filters.ts selectedEvaluationIds). Getters: hasUnsavedFiles, isFileDirty.
Actions: markFileDirty, markFileSaved, clearDirtyFiles. removeFile clears
dirty state. reset() clears dirty state.

Refactor: allFiles getter simplified. loadedDatabaseIdsForFileId and
loadedFileIsForDatabaseIds converted from async Actions to synchronous
getters (databaseIdForFile, fileIdForDatabaseId) — they did no async work.
Callers updated. loadedDatabaseIds getter uses filter+map instead of
forEach+push. Loose equality (==) replaced with strict (===).

Authored by: Aaron Lippold<lippold@gmail.com>
Document why the as-cast in contextualExecutionProfiles is required:
narrowing SourcedContextualizedEvaluation.contains causes a circular
type reference that breaks vitest module loading. The cast is safe
because report_intake creates all evaluation profiles as
SourcedContextualizedProfile.

Authored by: Aaron Lippold<lippold@gmail.com>
Comprehensive Architecture Decision Record covering the attestation and
comment engine that replaces PR #8278. Three rounds of multi-agent review
(8 independent reviewers) with all findings resolved.

Covers: business workflows (attest NR controls, comment any control),
review lifecycle (Draft→InReview→Final), UX design (right-side review
panel, control row actions, notification bar), data architecture
(annotation store with O(1) indexed lookups, immutable original + overlay),
export/import (SAF CLI compatible attestation files + Heimdall annotation
bundles), CKL round-trip support, Phase 3 approval flow design.

Documents existing code inventory, prior art (checklistView branch),
and complete Phase 1-3 card breakdown with dependencies.

Authored by: Aaron Lippold<lippold@gmail.com>
…nalysis

Round 3 review findings (architecture + implementer + compliance):
- Split card B into B1 (forms+save) and B2 (history+nav+a11y)
- Added A2 card (fileIdForControl public getter)
- ReviewPanel mounts in Results.vue, not ControlTable
- Drawer coordination via ui store flag
- Failed→NA is must-have (ISSO routine workflow)
- Severity Override + Finding Details fields added to §3.1.3
- XSS security note: all user text via {{ }}, never v-html
- Export attestations (H) marked must-have (only Phase 1 persistence)
- CKL round-trip detection (J) deferred to Phase 2
- Changelog (§4.6) deferred to Phase 2
- attestationCanBeAdded is private — use status check instead
- originalStatuses baseline added to FileAnnotationState

checklistView branch analysis:
- Cataloged 8 components: 3 high-reuse (edit form, rules table, severity override)
- Store analysis: SearchEntry<T> with negation is reusable for NR filtering
- Card mapping: ~2 of 12 cards benefit (B1 widgets only — data model is antipattern)
- Lessons: branch failed due to scope creep, broken filtering, CKL-only model

Authored by: Aaron Lippold<lippold@gmail.com>
…ferences

Split Phase 3 POA&M card into:
- S1: FromHDFToPOAMMapper in hdf-converters (library, reusable by SAF CLI)
- S2: Heimdall GUI "Export as POA&M" button (depends on S1)

References mitre/ckl2POAM for severity/status mapping logic and
mitre/emasser for eMASS API field mappings.

Authored by: Aaron Lippold<lippold@gmail.com>
…position rules

Which attestation dispositions are POA&M items? Open = yes, Risk Accepted =
yes (with AO justification), Not Applicable = probably not (dispositioned).
ckl2POAM maps CKL statuses but doesn't account for attestation types.
Must research NIST SP 800-37 Rev 2 §3.4 before implementing mapper.

Authored by: Aaron Lippold<lippold@gmail.com>
… UI → integration

Each layer fully testable before the next starts. Opposite of PR #8278
(UI first) and checklistView (everything at once).

Authored by: Aaron Lippold<lippold@gmail.com>
Add AnnotationStore module for attestation records + comment log per file.
Object-replacement reactive pattern for Vue 2 (no Vue.set/Map/Set).
addAttestation auto-populates updated/control_id per ADR §3.1.1.
addCommentWithControl patches live display via cross-module commit.
Inline dirty-marking in updateControlDescription (fixes nested mutation).
22 tests including cross-module behavior verification.

Authored by: Aaron Lippold<lippold@gmail.com>
Add toAttestationJson, toAttestationYaml (yaml pkg), toHeimdallBundle,
toAttestationXlsx (@e965/xlsx with parseXLSXAttestations round-trip format).
Wire exportAttestations store action with saveAs per format.
XLSX uses sheet 'attestations' matching import column format.
16 serialization tests + 6 store action tests with file-saver mock.

Authored by: Aaron Lippold<lippold@gmail.com>
Add prepareEvaluationForCklExport to hdf-converters — pure function that
deep-clones evaluation, applies attestations via addAttestationToHDF, and
syncs description edits to CKL passthrough vulns via syncChecklistVulnComments.
Add buildEditsMapFromProfiles helper for format-agnostic edit extraction.
ExportCKLModal simplified to thin caller with .then/.catch/.finally chain.
27 hdf-converters tests + 2 frontend integration tests.

Authored by: Aaron Lippold<lippold@gmail.com>
Enhance cleanUpFilename with optional targetExtension param that strips
matching extension before appending (case-insensitive). Also strips colons
(invalid on Windows). Remove 3 duplicate cleanUpFilename functions from
ExportCSVModal, ExportJson, ExportHTMLModal — centralized in export_util.ts.
Updated: CKL, CSV, JSON, XCCDF, ASFF, HTML modals.
Verified no-change: CAAT (date-based), NIST (sheet name), Splunk (API).
16 unit tests covering extension normalization + edge cases.

Authored by: Aaron Lippold<lippold@gmail.com>
… dialog + ADR updates

Environment:
- Rename PORT → HEIMDALL_BACKEND_PORT across all config (12-factor compliance)
- Backend start:dev loads own .env via dotenv (no global injection)
- Root start:dev removed dotenv wrapper — each service owns its env
- Frontend reads HEIMDALL_FRONTEND_PORT + HEIMDALL_BACKEND_PORT from .env.development.local
- Deprecation warning logged when legacy PORT is used
- Updated: Dockerfile, docker-compose.yml, docker-compose.okta.yml, .env-example,
  .env-ci, README.md, RPM packaging docs + CLI

UI wiring (9go.26):
- Vuetify ConfirmDialog component + Vue.observable confirm service
- Router beforeEach dirty-check guard with confirm dialog
- DB save icon changed to mdi-database-arrow-up
- Removed native beforeunload handler

ADR-001 updates:
- Added severity/finding details to FileAnnotationState (§5.2.3)
- Added cards G2 (HDF export) + G3 (severity pipeline) to §7.1
- Updated dependency graph + sp totals

Authored by: Aaron Lippold<lippold@gmail.com>
…ng changes

Regenerated style.css and embedded-assets.ts from yarn build in
libs/hdf-converters. Required after adding prepareEvaluationForCklExport
and buildEditsMapFromProfiles to description-editing.ts.

Authored by: Aaron Lippold<lippold@gmail.com>
…9go.49)

Add optional prettyPrint param to toCkl(). Default returns raw JSONIX XML
(31ms vs 69ms on 261-control RHEL8 file). Pretty-print available via
toCkl({prettyPrint: true}) for debugging. Existing tests updated to use
prettyPrint to match expected fixtures. 4 new performance tests verify
speedup, valid XML, and VULN count parity. All exports validated against
DISA CKL Schema V2.5 via xmllint.

Authored by: Aaron Lippold<lippold@gmail.com>
…script to .mts (9go.46)

Add "type": "commonjs" to 5 package.json files (hdf-converters, inspecjs,
common, password-complexity, backend) to eliminate MODULE_TYPELESS_PACKAGE_JSON
warning. Rename convert-to-embedded-strings.ts to .mts for explicit ESM marking
since Node 24 treats .mts as always-ESM regardless of package type field.

Authored by: Aaron Lippold<lippold@gmail.com>
…9go.43)

Apply attestations to all raw-data export formats via clone+addAttestationToHDF:
- ExportJson: async populate_files() with clone before JSON.stringify
- ExportXCCDF: clone before FromHDFToXCCDFMapper + markFileSaved/catch chain
- ExportASFF: clone before FromHdfToAsffMapper + markFileSaved/catch chain
- ExportCSV: Attestation Status + Attestation Explanation columns added

ADR updated with §5.4.2a (XCCDF/ASFF), §5.4.2b (CSV), §5.4.2c (coverage matrix).
9 integration tests covering clone+apply behavior and CSV column lookup.

Authored by: Aaron Lippold<lippold@gmail.com>
Add sourceFormat field to InspecFile type, threaded through loadFile→
loadText→loadExecJson. Native HDF='inspec-json', profiles='inspec-profile',
converted files get fingerprint() result (INPUT_TYPES value). DB-loaded
files default appropriately; legacy loadExecJson without sourceFormat
remains undefined.

6 behavioral tests using real store methods + ubuntu_profile.json fixture.

Authored by: Aaron Lippold<lippold@gmail.com>
…ecisions

Disable rules with known dangerous auto-fix behavior:
- perfectionist/sort-modules: breaks circular type declarations
- unicorn/prefer-spread: converts _.concat() to [..._] (spreading lodash)
- e18e/prefer-spread-syntax: same as above
- consistent-type-definitions: converts interface→type, breaks circular refs
- import-x/namespace: false positives on CJS lodash
- n/no-missing-import, n/no-unpublished-import: can't resolve TS/monorepo
- n/no-unsupported-features/es-syntax: wrong Node version default

Reconfigure:
- consistent-type-imports: inline-type-imports (prevents unsafe import splitting)
- restrict-template-expressions: allow number + boolean + nullish
- unicorn/filename-case: allow kebab + snake + camel + pascal

Add docs/development/eslint-config-decisions.md with rationale, upstream
issue links, and auto-fix safety guide for each rule.

Authored by: Aaron Lippold<lippold@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

}
});
const fullUrl = `${host_url.replace(/\/$/v, '')}/rest/currentUser`;
const result = await axios.get(fullUrl, { headers: { 'x-apikey': `accesskey=${accesskey}; secretkey=${secretkey}` } });
}

/** Sets the local storage value to the given value, stringified */
set(val: T): void {
const nv = JSON.stringify(val);
window.localStorage.setItem(this.storageKey, nv);
globalThis.localStorage.setItem(this.storageKey, nv);
@@ -134,58 +136,58 @@
if (guessOptions.filename.toLowerCase().endsWith('.nessus')) {
return INPUT_TYPES.NESSUS;
} else if (
guessOptions.data.match(/xmlns.*http.*\/xccdf/) || // Keys matching (hopefully) all xccdf formats
guessOptions.filename.toLowerCase().indexOf('xccdf') !== -1
XCCDF_XMLNS_RE.test(guessOptions.data)
) {
return INPUT_TYPES.XCCDF;
} else if (
guessOptions.data.match(/<netsparker-.*generated.*>/) ||
guessOptions.data.match(/<invicti-.*generated.*>/)
NETSPARKER_RE.test(guessOptions.data)
Comment thread libs/hdf-converters/src/utils/fingerprinting.ts Fixed
Tests for 3 reverse mappers (CAAT, HTML, XCCDF) import
embedded-assets.ts which is generated by tailwindcss + convert-to-
embedded-strings.mts. The file is gitignored (841KB, deterministic
from committed template files). Previously, tests silently failed
on fresh clones or after rebases when the file was missing.

Fix: vitest globalSetup checks for the file and generates it if
absent (vitest's documented pre-test lifecycle hook). Also adds
pretest/pretest:ci npm hooks as belt-and-suspenders for yarn test
and yarn run test:ci. Extracts the generation command into a
DRY generate:assets script used by prebuild, pretest, and
globalSetup.

Authored by: Aaron Lippold<lippold@gmail.com>
… of inline literal

AC verify caught: 3 mappers imported the helper but never called it,
using hand-written {sourceFormat, toolVersion} inline instead. Dead
imports caused lint errors. Fixed by converting to actual helper calls:
- snyk: nested-key → single transformer wrapping createHeimdallPassthrough
- ionchannel: nested-key → single transformer with _.get for metadata path
- veracode: keep nested-key (complex MappedTransform), call helper for .heimdall

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold aaronlippold had a problem deploying to heimdall2-feature-attes-fdeulf June 23, 2026 19:50 Failure
…pressions, style

Fix lint errors on Session 5 batch mappers that had DEFAULT_PROFILE_FIELDS
wired but were not individually lint-cleaned:

- withRaw → shouldIncludeRaw (unicorn/consistent-boolean-name) in
  dbprotect, nikto, sarif, dependency-track, scoutsuite, jfrog-xray,
  gosec, twistlock, veracode, snyk, trufflehog
- String() wraps for _.get() in template literals
  (restrict-template-expressions) in gosec, jfrog-xray, dependency-track,
  veracode, snyk, trufflehog
- Remove useless template literals (unicorn/no-useless-template-literals)
  in gosec, twistlock, veracode, trufflehog
- Extract nested call in scoutsuite constructor (unicorn/max-nested-calls)
- Remove useless else in snyk (unicorn/no-useless-else)
- Prefer ternary in twistlock title (unicorn/prefer-ternary)

All 11 files now pass eslint --max-warnings 0. 212 tests pass.

Authored by: Aaron Lippold<lippold@gmail.com>
…ct-injection)

Convert MappingData from bracket-notation Record access to Map.get()
per eslint-config-decisions.md documented pattern. Eliminates
detect-object-injection warning without suppression.

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold aaronlippold had a problem deploying to heimdall2-feature-attes-fdeulf June 23, 2026 22:31 Failure

export function setup(): void {
if (!existsSync(EMBEDDED_ASSETS)) {
execSync('yarn generate:assets', {
…dFixture DRY

- ESLint auto-fix: comma-dangle, object-curly-spacing, sort-objects,
  object-curly-newline, arrow-parens, spaced-comment, indent
- Normalize encoding identifier 'utf-8' to 'utf8' across all test files
  (unicorn/text-encoding-identifier-case)
- Add loadFixture() helper to test/utils.ts — DRYs the repeated
  JSON.parse(fs.readFileSync(path, {encoding: 'utf8'})) pattern
- Replace nested fixture loading with loadFixture() in 22 test files,
  fixing unicorn/max-nested-calls (120 → 13 remaining)
- Add normalizeASFF() compose helper in asff_reverse_mapper.spec.ts
  to flatten omitASFFVersions(omitASFFTimes(omitASFFTitle(...))) nesting
- Fix ZAP test URLs: restore http:// fixture data URLs that auto-fix
  incorrectly changed to https:// (with targeted eslint-disable)

Authored by: Aaron Lippold<lippold@gmail.com>
…rals, style

- Remove useless else after return/throw/continue (unicorn/no-useless-else)
  — 30 auto-fixed, 11 remaining need manual review
- Remove useless template literal expressions (unicorn/no-useless-template-literals)
  — replace backtick wrapping with direct value or String()
- ESLint auto-fix: comma-dangle, object-curly-spacing, sort-objects,
  perfectionist/sort-imports, consistent-type-imports

Total lint reduction this session: 2815 → 311 (89% reduction)
Remaining 311 are structural issues requiring per-case review or
blocked on 4qm.44.5 (parseHtml async init refactor).

Authored by: Aaron Lippold<lippold@gmail.com>
- Rename withRaw → shouldIncludeRaw in 10 remaining mappers
  (unicorn/consistent-boolean-name)
- Rename other boolean params: verifySSLCertificates → shouldVerify,
  containsChecklist → hasChecklist, returnWorkBook → shouldReturnWorkBook,
  collapseResults → shouldCollapseResults, addPrefix → shouldAddPrefix,
  collapse → shouldCollapse, expired → isExpired, condition → shouldInclude,
  showAsPercentage → shouldShowAsPercentage, dateOverride → hasDateOverride,
  isoTime → isIsoTime, sizeCheck → shouldCheckSizeOnly
- Wrap _.get() return values with String() in template literals
  (restrict-template-expressions) across 8 files
- Remove redundant `as string` cast after _.isString() narrowing

Authored by: Aaron Lippold<lippold@gmail.com>
- Remove all remaining useless else after return/throw (11 files)
- Convert .then()/.catch() chains to try/await/catch in sonarqube,
  ionchannel, and splunk reverse mappers (14 instances)
- Add String() wraps for restrict-template-expressions in asff,
  conveyor, fortify, zap, and global utils

Authored by: Aaron Lippold<lippold@gmail.com>
- Replace Number.parseFloat() with Number() (unicorn/prefer-number-coercion)
  in fortify, cci-nist, splunk, compliance utils
- Replace `key in obj` with Object.hasOwn(obj, key) for dynamic property
  existence checks (unicorn/no-computed-property-existence-check)
  in asff, aws-config, nikto-nist, global utils

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold aaronlippold had a problem deploying to heimdall2-feature-attes-fdeulf June 24, 2026 03:13 Failure
}
if (
NETSPARKER_RE.test(guessOptions.data)
|| INVICTI_RE.test(guessOptions.data)
…use, early return

- Remove unused fs imports after loadFixture migration (3 test files)
- Replace JSON.parse(JSON.stringify()) with structuredClone (description-editing tests)
- Add { cause: error } to re-thrown errors (preserve-caught-error, sonarqube)
- Remove unnecessary await on non-Promise toHTML() (html reverse mapper tests)
- Use logical assignment ||= (prisma-mapper)
- Use this in static method (caat reverse mapper)
- Rename MetaData → Metadata (consistent-compound-words, splunk-mapper)
- Early return guard (checklist-metadata-utils)
- Replace duplicate loadJsonFile with shared loadFixture (checklist_performance)

Authored by: Aaron Lippold<lippold@gmail.com>
…ause

- Extract variables to reduce max-nested-calls in transformers,
  xccdf-results, and zap mappers
- Move regex literals to module scope (e18e/prefer-static-regex)
  in test utils, attestation tests, checklist perf, regen fixtures
- Rename MetaData → Metadata in splunk-mapper (consistent-compound-words)
- Add { cause: error } to sonarqube error re-throws
- Use logical ||= assignment in prisma-mapper
- Use this in static method for CAAT mapper
- Early return guard in checklist-metadata-utils
- Remove unused fs imports, use loadFixture, structuredClone

Authored by: Aaron Lippold<lippold@gmail.com>
…lint

- Deduplicate describe block names in anchore-grype, trufflehog,
  twistlock, zap, and attestation test files (vitest/no-identical-title)
- Extract regex to module scope in test utils, attestation tests,
  checklist perf, regen fixtures (e18e/prefer-static-regex)
- Extract nested calls in transformers, xccdf-results, zap mappers
- Use structuredClone, remove unused fs imports, logical ||= assignment,
  early return, this in static, error cause preservation

Authored by: Aaron Lippold<lippold@gmail.com>
@aaronlippold aaronlippold temporarily deployed to heimdall2-feature-attes-fdeulf June 24, 2026 03:48 Inactive
| **CKL** | Checklist — DISA's legacy XML format used by STIG Viewer 2 for recording STIG review results. Uses statuses: Not A Finding, Open (Finding), Not Applicable, Not Reviewed. |
| **CKLB** | Checklist JSON — DISA's newer JSON format introduced by STIG Viewer 3 (SV3). File extension `.cklb`. Uses statuses: `not_a_finding`, `open`, `not_applicable`, `not_reviewed`. |
| **SV3** | STIG Viewer 3 — DISA's tool for reviewing STIGs and producing checklists |
| **HDF / OHDF** | Heimdall Data Format / Outcome-based HDF — JSON output format from InSpec and other scanners |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the O in OHDF does not stand for 'outcome-based'

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Glossary now reads: HDF / OHDF — Heimdall Data Format / OASIS HDF. Updated in both ADR-001 and ADR-003.

### 1.3 The CKLB Schema (SV3 v1.0)

The schema defines a JSON structure with:
- **Top level:** `title` (filename, **required**), `id` (UUID, **required**), `cklb_version` ("1.0"), `target_data`, `stigs[]`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing three fields that are used by SV internally but we've always said that we will bring through all data regardless

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§1.3 expanded with additional fields. Added note that generated types from the CKLB JSON schema (§6.5, card 1af.5) are authoritative — all schema fields must be brought through, no silent drops. §3.4 updated with expanded passthrough field lists.


The schema defines a JSON structure with:
- **Top level:** `title` (filename, **required**), `id` (UUID, **required**), `cklb_version` ("1.0"), `target_data`, `stigs[]`
- **Per STIG:** `stig_name`, `display_name`, `stig_id`, `release_info`, `uuid`, `size`, `rules[]`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a field here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§1.3 and §3.4 expanded. The generated CKLB types (card 1af.5) will be the definitive field list — any field in the schema that we missed will be caught at type generation time.


Build a CKLB converter (forward + reverse) by extracting the shared checklist domain logic from the existing CKL mapper into a common module, then adding a thin CKLB serialization layer. The extraction and the new converter are one body of work — not "DRY first, then build."

### 2.1 Architecture: Shared Intermediate (Option B)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is option a?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. §2.2 now presents all four options: A (standalone — rejected, DRY violation), B (shared intermediate — rejected, unnecessary abstraction), C (CKLB→CKL→HDF — rejected, lossy), D (CKLB as canonical — chosen).

**Option A (fully standalone CKLB mapper):** Duplicates ~400 lines of validated severity/status/CCI/comment logic. Guarantees the two mappers drift over time. Violates DRY.

**Option C (CKLB→CKL→HDF, convert internally):** Forces CKLB through XML's lossy `STIG_DATA` key-value representation. Discards CKLB-native fields. Strictly more lossy and more fragile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no option d presented of ckl -> cklb -> hdf which is what i'm proposing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone loads an old ohdf derived from an old ckl, will the process be backward compatible?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted as Option D — now the chosen architecture. §2.1 revised: CKL XML → CKLB object → HDF. CKLB is the canonical intermediate. CKL becomes a thin XML adapter. ADR-003 fully rewritten for this approach. Commit 2b7770adb.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — fully backward compatible. Added §2.5 to ADR-003 addressing this directly. Option D changes the internal pipeline (XML → CKLB object → HDF instead of XML → JSONIX intermediate → HDF) but the HDF output is byte-for-byte identical. Existing CKL fixture regression tests enforce this. Existing HDF files with CKL passthrough data continue working — passthrough.checklist key and shape are preserved unchanged.

### 5.4 The JSONIX Type Coupling (Critical Extraction Risk)

The shared intermediate types are currently **defined in terms of JSONIX types** from `checklistJsonix.ts`:
- `ChecklistVuln = Omit<Vuln, 'status' | 'stigdata'> & {...}` — extends the JSONIX `Vuln` type

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems to have a slight misunderstanding of where these types are coming from - jsonix just derives these types from the og ckl type

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected. §5.3 clarified — JSONIX derives its types from the original CKL XSD. The types are XSD-derived, not invented. Under Option D, CKLB generated types replace the JSONIX-derived types as the canonical intermediate.


**Fix:** Relocate the domain enums (`Assettype`, `Role`, `Techarea`, `Severityoverride`), the base `Vuln` shape, the `Asset` type, and their transitive dependencies (`Status`, `StigdatumElement`, `Vulnattribute`) into `checklist-common/types.ts`. Have `checklistJsonix.ts` re-import/alias them. Constraint: `checklist-common/types.ts` must import NOTHING from `ckl-mapper/` — enforce this with an ESLint `no-restricted-imports` rule or a build test.

### 5.4a The Instance-Method Problem (Critical Extraction Challenge)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a massive headache during the refactor process

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eliminated by Option D. No shared base class extraction, no JSONIX type relocation, no ChecklistConverterBase. Domain functions move directly to cklb-mapper as plain functions. Cards 1af.1, 1af.2, 1af.3 closed as obsolete.


- **`DEFAULT_PROFILE_FIELDS`** (ADR-002): Wire into `ChecklistMapper` (shared intermediate→HDF) and `CklbMapper`. The current `ChecklistMapper` hand-declares `attributes/groups/sha256/status/supports` — it is NOT in ADR-002's list of wired mappers and should be.
- **`createHeimdallPassthrough('cklb', ...)`** (ADR-002 / 9go.52): Use the standard passthrough helper.
- **Regen tool** (ADR-002): Add CKLB fixtures to the `regenerate-fixtures.mts` registry. Use the same `omitVersions + toEqual` fixture-comparison pattern as all other mappers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this regen tool

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explained in §6.5 with link to ADR-002. The fixture regeneration tool lives at libs/hdf-converters/scripts/regenerate-fixtures.mts — supports --validate, --dry-run, --revert, and size guards for safe mapper refactoring. CKLB fixtures will be added to its registry.


---

## 8. Edge Cases

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some good callouts here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — kept and carried forward into the revised ADR.


### 1.4 Key Insight

**CKLB is structurally the existing CKL intermediate object with renamed fields and no XML layer.** Every piece of format-agnostic logic the CKL mapper already has — status mapping, severity↔impact, CCI↔NIST, finding-details parsing, structured-comment parsing, multi-STIG parent-profile synthesis — applies unchanged to CKLB. The difference is serialization: CKL needs JSONIX for XML; CKLB is `JSON.parse`/`JSON.stringify`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CKLB is structurally the existing CKL intermediate object with renamed fields and no XML layer.

this is what i've been saying this entire time. so instead of having to continue dealing with the intermediate object let's replace it outright with just cklb instead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and adopted as the core architecture decision. Your insight is cited directly in §1.4. Option D (CKLB as canonical intermediate) is now the chosen approach. CKL becomes a thin XML adapter. The entire shared extraction (Option B) is eliminated. Commit 2b7770adb.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4 New Bugs (required ≤ 0)
3 New Vulnerabilities (required ≤ 0)
70 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE


**Effect on display (immediate, in-memory):**
- Comment entry added to annotation store comment log (audit trail — append-only, accumulates across review cycles)
- `control.data.descriptions.comments` and `control.hdf.descriptions.comments` patched for live display (display optimization, see §5.1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checking - if the source hdf already has a passthrough with checklist data, are we maintaining the comments fields here as well? iirc, it is used to accurately reconstitute all of the checklist upon export.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah! I think I see it in 3.3. Correct?

```

**On CKL import, Heimdall must:**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Is the import treated as an entirely new evaluation in Heimdall, or somehow associated with the earlier one in Heimdall?
  • If the later, what if someone commented on the earlier one in Heimdall after export. How are the comments from the externally modified one reconciled?

```
User imports a CKL file that was previously exported and edited in STIG Viewer:
1. Normal CKL import runs (checklist-mapper.ts) → produces HDF
2. After import, if the file was previously loaded in Heimdall:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment from above - how does it know it was previously loaded?

- Does NOT change status or compliance score

**Effect on export:**
- `descriptions.comments` already contains the latest comment text from live mutation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clarify, which are you proposing? Are the comments field in the OHDF be an append or replace with latest

]
```

**Comment file format** (Heimdall extension — separate file, NOT mixed with attestations):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I'm requesting it, but are you proposing some sort of export for comments (comment log?)

@mergify

mergify Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

This pull request has a conflict. Could you fix it @aaronlippold?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants