Skip to content
Draft
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3c6c58a
feat: add write-side description helpers + CKL export-time sync to hd…
aaronlippold Jun 16, 2026
ef1a9a7
chore: fix isolatedModules type export errors + silence sass deprecat…
aaronlippold Jun 16, 2026
11412b0
fix: use import type for type-only imports across frontend (isolatedM…
aaronlippold Jun 16, 2026
e8b54ca
refactor: centralize HeimdallToolsVersion in shared utils/global.ts
aaronlippold Jun 16, 2026
94c268c
chore: silence sass deprecation warnings + disable dev server progres…
aaronlippold Jun 16, 2026
88ac9da
refactor: AppConfig singleton + replace console.log with NestJS Logger
aaronlippold Jun 16, 2026
d278528
feat: generic updateControlDescription mutation + dirty tracking in d…
aaronlippold Jun 16, 2026
da43b44
docs: add safety comment to SourcedContextualizedProfile cast
aaronlippold Jun 16, 2026
1f1f569
docs: ADR-001 GUI Attestation & Comment Engine — full design document
aaronlippold Jun 17, 2026
8d28a18
docs: ADR-001 final updates — review round 3 + checklistView branch a…
aaronlippold Jun 17, 2026
81e21bf
docs: ADR-001 add POA&M mapper card (S1→S2 split) + eMASS/ckl2POAM re…
aaronlippold Jun 17, 2026
3b8094a
docs: ADR-001 add POA&M research requirement — verify NIST 800-37 dis…
aaronlippold Jun 17, 2026
065877a
docs: ADR-001 add explicit implementation strategy — store → export →…
aaronlippold Jun 17, 2026
5c1f221
feat: annotation Vuex store with O(1) indexed lookups (9go.19)
aaronlippold Jun 17, 2026
d2a8804
feat: export attestation files — JSON/YAML/XLSX/bundle (9go.28)
aaronlippold Jun 17, 2026
8689951
feat: CKL export-time sync + attestation application (9go.27)
aaronlippold Jun 17, 2026
3952930
fix: prevent double file extensions on re-export — all 9 modals (9go.45)
aaronlippold Jun 17, 2026
fa8df03
chore: standardize env vars (HEIMDALL_BACKEND_PORT) + Vuetify confirm…
aaronlippold Jun 17, 2026
d4aeee1
chore: rebuild hdf-converters embedded assets after description-editi…
aaronlippold Jun 17, 2026
31074aa
perf: skip xmlFormat in toCkl() by default — 2.2x faster CKL export (…
aaronlippold Jun 17, 2026
4a840b3
chore: add explicit type:commonjs to all packages + rename ESM build …
aaronlippold Jun 17, 2026
62bb927
feat: wire attestation application into JSON/XCCDF/ASFF/CSV exports (…
aaronlippold Jun 17, 2026
ca5e9da
feat: persist sourceFormat on InspecFile during file load (9go.47)
aaronlippold Jun 17, 2026
bcff2c3
chore: fix eslint config — disable unsafe auto-fix rules + document d…
aaronlippold Jun 17, 2026
d9a8a23
chore: fix tsconfig for inspecjs + hdf-converters — proper base/build…
aaronlippold Jun 17, 2026
097f816
chore: inspecjs lint fixes — named regex groups, enum comparisons, ty…
aaronlippold Jun 17, 2026
52da101
chore: hdf-converters manual lint fixes — type annotations, enum memb…
aaronlippold Jun 17, 2026
8c3548f
style: eslint auto-fix across all packages — 39K+ style issues resolved
aaronlippold Jun 17, 2026
a491a09
feat: source format badge on EvaluationInfo + format label/color util…
aaronlippold Jun 17, 2026
9b244cb
style: eslint auto-fix on attestation engine frontend files
aaronlippold Jun 17, 2026
e063af0
feat: createHeimdallPassthrough helper for sourceFormat metadata (9go…
aaronlippold Jun 17, 2026
46d2a68
chore: additional eslint config fixes — n/ builtins, unused-vars, res…
aaronlippold Jun 18, 2026
0500831
chore: hdf-converters lint fixes — 200+ manual fixes across 38 files
aaronlippold Jun 18, 2026
0493f1c
chore: inspecjs lint fixes — enum members, toSorted, optional chain, …
aaronlippold Jun 18, 2026
e5d72fb
chore: fix 7 more lint errors — promise returns, function scoping, re…
aaronlippold Jun 18, 2026
604e5cb
fix: wire eslint-import-resolver-typescript into eslint config
aaronlippold Jun 18, 2026
e9440f4
fix: repair 3 production bugs introduced by lint auto-fix
aaronlippold Jun 18, 2026
c9d003d
test: regenerate test fixtures to match corrected mapper output
aaronlippold Jun 18, 2026
ae5d758
test: add service availability checks + env var config + coverage
aaronlippold Jun 18, 2026
c9f76a3
feat: add DEFAULT_PROFILE_FIELDS constant to BaseConverter (4qm.2)
aaronlippold Jun 18, 2026
c1191e2
fix: gitignore generated build artifacts — style.css + embedded-asset…
aaronlippold Jun 18, 2026
7972a96
fix: DEFAULT_PROFILE_FIELDS type + full lint cleanup on base-converte…
aaronlippold Jun 18, 2026
4c871fd
feat: fixture regeneration tool with --validate for safe mapper refac…
aaronlippold Jun 18, 2026
f38e0ff
refactor: wire DEFAULT_PROFILE_FIELDS into anchore-grype-mapper + ful…
aaronlippold Jun 21, 2026
e38bd93
fix: regen tool — snyk JSON.parse bug, size guard, regression tests, …
aaronlippold Jun 22, 2026
b2a31ea
refactor: wire DEFAULT_PROFILE_FIELDS into 14 forward mappers + regen…
aaronlippold Jun 22, 2026
5609c6e
docs: ADR-002 — DRY hdf-converters refactoring plan (profile defaults…
aaronlippold Jun 22, 2026
728b85f
feat: wire createHeimdallPassthrough into all 25 forward mappers (9go…
aaronlippold Jun 23, 2026
491493c
chore: regenerate fixtures with passthrough.heimdall metadata
aaronlippold Jun 23, 2026
46f1e8a
docs: ADR-003 — CKLB (STIG Viewer 3) converter design
aaronlippold Jun 23, 2026
c8060c7
docs: update ADR-001 per Will Dower review — simplify scope and UX
aaronlippold Jun 23, 2026
cea0c8f
fix: ensure generated embedded-assets.ts exists before tests run
aaronlippold Jun 23, 2026
a4d8bea
fix: snyk/ionchannel/veracode — use createHeimdallPassthrough instead…
aaronlippold Jun 23, 2026
6046e4a
refactor: lint-clean 11 forward mappers — boolean naming, template ex…
aaronlippold Jun 23, 2026
3fd7bcb
fix: checkov — use Map.get() for mapping lookup (security/detect-obje…
aaronlippold Jun 23, 2026
e11a69d
style: lint-clean hdf-converters test files — auto-fix, encoding, loa…
aaronlippold Jun 24, 2026
7162286
refactor: lint-clean hdf-converters src — useless else, template lite…
aaronlippold Jun 24, 2026
04cc810
refactor: boolean naming + String() wraps across hdf-converters src
aaronlippold Jun 24, 2026
890b188
refactor: remove useless else, convert .then() to await, String() wraps
aaronlippold Jun 24, 2026
b0d339e
refactor: Number() coercion + Object.hasOwn() for dynamic key checks
aaronlippold Jun 24, 2026
d061c42
refactor: misc lint fixes — unused imports, structuredClone, error ca…
aaronlippold Jun 24, 2026
9457aca
refactor: extract nested calls, static regex, compound words, error c…
aaronlippold Jun 24, 2026
bac689d
refactor: fix duplicate test names, static regex, nested calls, misc …
aaronlippold Jun 24, 2026
feb027b
Merge branch 'master' into feature/attestation-comment-engine
aaronlippold Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ RUN yarn build

FROM $BASE_CONTAINER AS app

# Default backend port — override with HEIMDALL_BACKEND_PORT env var
EXPOSE 3000

ARG NODE_ENV=production
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,10 @@ If you would like to change Heimdall to your needs, you can use Heimdall's 'Deve

You can also manually edit the `apps/backend/.env` file in a text editor and set additional optional configuration values. For more info on configuration values see [Enviroment Variables Configuration](https://github.com/mitre/heimdall2/wiki/Environment-Variables-Configuration).

**Port configuration:** The backend listen port is set via `HEIMDALL_BACKEND_PORT` (defaults to 3000). The frontend dev server port is set via `HEIMDALL_FRONTEND_PORT` in `apps/frontend/.env.development.local` (defaults to 8080). The legacy `PORT` env var is deprecated and will be removed in the next minor release — use `HEIMDALL_BACKEND_PORT` instead.

> [!NOTE]
> The .env file in the root repository is for the Docker deployment of the Heimdall application. Running a local build will use the .env file in the `apps/backend` directory for the database configurations.
> The .env file in the root repository is for the Docker deployment of the Heimdall application. Running a local build will use the .env file in the `apps/backend` directory for the database configurations. The frontend dev server reads its own env from `apps/frontend/.env.development.local`.

6. Build the project:

Expand Down
4 changes: 3 additions & 1 deletion apps/backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
CLASSIFICATION_BANNER_TEXT=<If a sensitivity classification banner should be shown to users, for example FOUO (if nothing is provided, no banner is shown)>
CLASSIFICATION_BANNER_TEXT_COLOR=<The color of the text on the sensitivity classification banner, if enabled (defaults to white)>
CLASSIFICATION_BANNER_COLOR=<The color of the sensitivity classification banner, if enabled (defaults to red)>
HEIMDALL_FRONTEND_PORT=<Dev server listen port (defaults to 8080). Set in apps/frontend/.env.development.local for local development.>
HEIMDALL_SERVER_MODE=<Enable API proxy to backend in dev (defaults to false). Set in apps/frontend/.env.development.local.>

## Backend
NODE_ENV=<development, production, or test (no default, must be set)>
PORT=<Port that the app starts up on (if nothing is provided, defaults to 3000)>
HEIMDALL_BACKEND_PORT=<Port that the backend listens on (defaults to 3000). Falls back to PORT for backward compatibility — PORT is DEPRECATED and will be removed in the next minor release.>
ADMIN_EMAIL=<email for default admin user (if nothing is provided, defaults to admin@heimdall.local)>
ADMIN_USES_EXTERNAL_AUTH=<if the default admin user uses alternative/external authentication (if nothing is provided, defaults to false)
ADMIN_PASSWORD=<Password for admin user (if nothing is provided, defaults to a randomly generated password that will only be shown on initial setup)>
Expand Down
22 changes: 12 additions & 10 deletions apps/backend/config/app_config.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
import {Logger} from '@nestjs/common';
import * as dotenv from 'dotenv';
import * as fs from 'fs';

export default class AppConfig {
private static instance: AppConfig;
private readonly logger = new Logger(AppConfig.name);
private envConfig: {[key: string]: string | undefined};

constructor() {
console.log('Attempting to read configuration file `.env`!');
private constructor() {
try {
this.envConfig = dotenv.parse(fs.readFileSync('.env'));
console.log('Read config!');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
this.envConfig = {};
// File probably does not exist
console.log('Unable to read configuration file `.env`!');
console.log('Falling back to environment or undefined values!');
this.logger.warn('.env not found, using environment variables only');
} else {
throw error;
}
}
if (this.parseDatabaseUrl()) {
console.log(
'DATABASE_URL parsed into smaller components (i.e. DATABASE_USER)'
);
this.parseDatabaseUrl();
}

static getInstance(): AppConfig {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig();
}
return AppConfig.instance;
}

set(key: string, value: string | undefined): void {
this.envConfig[key] = value;
}

get(key: string): string | undefined {
return process.env[key] || this.envConfig[key];
}

getExternalUrl(): string {
Expand Down Expand Up @@ -91,8 +93,8 @@
sslKey = this.get('DATABASE_SSL_KEY');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_KEY')!).isFile()) {
sslKey = fs.readFileSync(this.get('DATABASE_SSL_KEY')!);
} else {
throw new Error('SSL Key file does not exist');
}
Expand All @@ -104,8 +106,8 @@
sslCert = this.get('DATABASE_SSL_CERT');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CERT')!).isFile()) {
sslCert = fs.readFileSync(this.get('DATABASE_SSL_CERT')!);
} else {
throw new Error('SSL Cert file does not exist');
}
Expand All @@ -117,8 +119,8 @@
sslCA = this.get('DATABASE_SSL_CA');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CA')!).isFile()) {
sslCA = fs.readFileSync(this.get('DATABASE_SSL_CA')!);
} else {
throw new Error('SSL CA file does not exist');
}
Expand Down Expand Up @@ -162,7 +164,7 @@
return false;
} else {
const pattern =
/^(?:([^:\/?#\s]+):\/{2})?(?:([^@\/?#\s]+)@)?([^\/?#\s]+)?(?:\/([^?#\s]*))?(?:[?]([^#\s]+))?\S*$/;
const matches = url.match(pattern);

if (matches === null) {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/db/database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AppConfig from '../config/app_config';

const appConfig = new AppConfig();
const appConfig = AppConfig.getInstance();

module.exports = appConfig.getDbConfig();
3 changes: 2 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "2.13.0",
"license": "Apache-2.0",
"description": "Heimdall's backend which allows for data persistence, authentication, RBAC, and API access",
"type": "commonjs",
"repository": {
"type": "git",
"url": "https://github.com/mitre/heimdall2",
Expand All @@ -21,7 +22,7 @@
"lint:ci": "eslint --max-warnings 0",
"start": "node dist/src/main",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start --watch",
"start:dev": "dotenv -e .env -- nest start --watch",
"test": "vitest",
"test:ci": "vitest run"
},
Expand Down
5 changes: 1 addition & 4 deletions apps/backend/seeders/20200514154327-create-administrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,9 @@ module.exports = {
let envConfig = {};
try {
envConfig = dotenv.parse(fs.readFileSync('.env'));
console.log('Read config!');
} catch (error) {
if (error.code === 'ENOENT') {
// File probably does not exist
console.log('Unable to read configuration file `.env`!');
console.log('Falling back to environment or undefined values!');
console.warn('Seeder: .env not found, using environment variables only');
} else {
throw error;
}
Expand Down
116 changes: 58 additions & 58 deletions apps/backend/src/apikeys/apikey.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ForbiddenError} from '@casl/ability';
import { ForbiddenError } from '@casl/ability';
import {
BadRequestException,
Body,
Expand All @@ -11,67 +11,41 @@ import {
Query,
Request,
UseGuards,
UseInterceptors
UseInterceptors,
} from '@nestjs/common';
import {AuthnService} from '../authn/authn.service';
import {AuthzService} from '../authz/authz.service';
import {Action} from '../casl/casl-ability.factory';
import {GroupsService} from '../groups/groups.service';
import {APIKeysEnabled} from '../guards/api-keys-enabled.guard';
import {JwtAuthGuard} from '../guards/jwt-auth.guard';
import {LoggingInterceptor} from '../interceptors/logging.interceptor';
import {User} from '../users/user.model';
import {UsersService} from '../users/users.service';
import {ApiKeyService} from './apikey.service';
import {APIKeyDto} from './dto/apikey.dto';
import {CreateApiKeyDto} from './dto/create-apikey.dto';
import {DeleteAPIKeyDto} from './dto/delete-apikey.dto';
import {UpdateAPIKeyDto} from './dto/update-apikey.dto';
import { AuthnService } from '../authn/authn.service';
import { AuthzService } from '../authz/authz.service';
import { Action } from '../casl/casl-ability.factory';
import { GroupsService } from '../groups/groups.service';
import { APIKeysEnabled } from '../guards/api-keys-enabled.guard';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { LoggingInterceptor } from '../interceptors/logging.interceptor';
import { User } from '../users/user.model';
import { UsersService } from '../users/users.service';
import { ApiKeyService } from './apikey.service';
import { APIKeyDto } from './dto/apikey.dto';
import { CreateApiKeyDto } from './dto/create-apikey.dto';
import { DeleteAPIKeyDto } from './dto/delete-apikey.dto';
import { UpdateAPIKeyDto } from './dto/update-apikey.dto';

@UseInterceptors(LoggingInterceptor)
@UseGuards(APIKeysEnabled)
@Controller('apikeys')
@UseGuards(APIKeysEnabled)
@UseInterceptors(LoggingInterceptor)
export class ApiKeyController {
constructor(
private readonly authnService: AuthnService,
private readonly apiKeyService: ApiKeyService,
private readonly authz: AuthzService,
private readonly usersService: UsersService,
private readonly groupsService: GroupsService
private readonly groupsService: GroupsService,
) {}

@UseGuards(JwtAuthGuard)
@Get()
async findAPIKeys(
@Request() request: {user: User},
@Query('userId') userId: string,
@Query('groupId') groupId: string
): Promise<APIKeyDto[]> {
const abac = this.authz.abac.createForUser(request.user);

if (userId && groupId) {
throw new BadRequestException('Cannot specify both userId and groupId');
}

if (groupId) {
const group = await this.groupsService.findByPkBang(groupId);
ForbiddenError.from(abac).throwUnlessCan(Action.Read, group);
return this.apiKeyService.findAllForGroup(group);
} else {
const user = userId
? await this.usersService.findById(userId)
: request.user;
ForbiddenError.from(abac).throwUnlessCan(Action.Read, user);
return this.apiKeyService.findAllForUser(user);
}
}

@UseGuards(JwtAuthGuard)
@Post()
@UseGuards(JwtAuthGuard)
async createAPIKey(
@Request() request: {user: User},
@Body() createApiKeyDto: CreateApiKeyDto
): Promise<{id: string; apiKey: string}> {
@Request() request: { user: User },
@Body() createApiKeyDto: CreateApiKeyDto,
): Promise<{ apiKey: string; id: string }> {
const abac = this.authz.abac.createForUser(request.user);

let target;
Expand All @@ -94,24 +68,24 @@ export class ApiKeyController {
return this.apiKeyService.create(target, createApiKeyDto);
}

@UseGuards(JwtAuthGuard)
@Delete(':id')
@UseGuards(JwtAuthGuard)
async deleteAPIKey(
@Request() request: {user: User},
@Request() request: { user: User },
@Param('id') id: string,
@Body() deleteApiKeyDto: DeleteAPIKeyDto
@Body() deleteApiKeyDto: DeleteAPIKeyDto,
): Promise<APIKeyDto> {
const apiKeyToDelete = await this.apiKeyService.findById(id);
const abac = this.authz.abac.createForUser(request.user);

if (apiKeyToDelete.type === 'user') {
ForbiddenError.from(abac).throwUnlessCan(
Action.Update,
apiKeyToDelete.user
apiKeyToDelete.user,
);
} else if (apiKeyToDelete.type === 'group') {
const group = await this.groupsService.findByPkBang(
apiKeyToDelete.groupId
apiKeyToDelete.groupId,
);
ForbiddenError.from(abac).throwUnlessCan(Action.Update, group);
} else {
Expand All @@ -124,24 +98,50 @@ export class ApiKeyController {
return this.apiKeyService.remove(id);
}

@Get()
@UseGuards(JwtAuthGuard)
async findAPIKeys(
@Request() request: { user: User },
@Query('userId') userId: string,
@Query('groupId') groupId: string,
): Promise<APIKeyDto[]> {
const abac = this.authz.abac.createForUser(request.user);

if (userId && groupId) {
throw new BadRequestException('Cannot specify both userId and groupId');
}

if (groupId) {
const group = await this.groupsService.findByPkBang(groupId);
ForbiddenError.from(abac).throwUnlessCan(Action.Read, group);
return this.apiKeyService.findAllForGroup(group);
} else {
const user = userId
? await this.usersService.findById(userId)
: request.user;
ForbiddenError.from(abac).throwUnlessCan(Action.Read, user);
return this.apiKeyService.findAllForUser(user);
}
}

@Put('/:id')
@UseGuards(JwtAuthGuard)
async updateAPIKey(
@Request() request: {user: User},
@Request() request: { user: User },
@Param('id') id: string,
@Body() updateApiKeyDto: UpdateAPIKeyDto
@Body() updateApiKeyDto: UpdateAPIKeyDto,
): Promise<APIKeyDto> {
const apiKeyToUpdate = await this.apiKeyService.findById(id);
const abac = this.authz.abac.createForUser(request.user);
if (apiKeyToUpdate.type === 'group') {
const group = await this.groupsService.findByPkBang(
apiKeyToUpdate.groupId
apiKeyToUpdate.groupId,
);
ForbiddenError.from(abac).throwUnlessCan(Action.Update, group);
} else if (apiKeyToUpdate.type === 'user') {
ForbiddenError.from(abac).throwUnlessCan(
Action.Update,
apiKeyToUpdate.user
apiKeyToUpdate.user,
);
} else {
throw new BadRequestException('Unknown API key type');
Expand Down
52 changes: 24 additions & 28 deletions apps/backend/src/apikeys/apikey.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,53 +9,49 @@ import {
Model,
PrimaryKey,
Table,
UpdatedAt
UpdatedAt,
} from 'sequelize-typescript';
import {Group} from '../groups/group.model';
import {User} from '../users/user.model';
import { Group } from '../groups/group.model';
import { User } from '../users/user.model';

@Table
export class ApiKey extends Model {
@PrimaryKey
@AutoIncrement
@Column(DataType.STRING)
declare apiKey: string;

@AllowNull(false)
@Column(DataType.BIGINT)
declare id: string;
@Column(DataType.DATE)
@CreatedAt
declare createdAt: Date;

@ForeignKey(() => User)
@Column(DataType.BIGINT)
declare userId: string;
@BelongsTo(() => Group, { constraints: false })
declare group: Group;

@ForeignKey(() => Group)
@Column(DataType.BIGINT)
@ForeignKey(() => Group)
declare groupId: string;

@BelongsTo(() => User, {
constraints: false
})
declare user: User;

@BelongsTo(() => Group, {
constraints: false
})
declare group: Group;
@AllowNull(false)
@AutoIncrement
@Column(DataType.BIGINT)
@PrimaryKey
declare id: string;

@Column(DataType.STRING)
declare name: string;

@Column(DataType.STRING)
declare apiKey: string;

@Column(DataType.STRING)
declare type: string;

@CreatedAt
@AllowNull(false)
@Column(DataType.DATE)
declare createdAt: Date;

@UpdatedAt
@AllowNull(false)
@Column(DataType.DATE)
declare updatedAt: Date;

@BelongsTo(() => User, { constraints: false })
declare user: User;

@Column(DataType.BIGINT)
@ForeignKey(() => User)
declare userId: string;
}
Loading