docs re-org, audit fixes, build fixes
This commit is contained in:
@@ -26,7 +26,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { inject } from '@angular/core';
|
||||
|
||||
import { SecretAlertSettings, SecretAlertDestination } from '../../models';
|
||||
import { SecretAlertSettings, SecretAlertDestination, SecretSeverity } from '../../models';
|
||||
import { SecretDetectionSettingsService } from '../../services';
|
||||
|
||||
@Component({
|
||||
@@ -403,7 +403,16 @@ export class AlertDestinationConfigComponent {
|
||||
private readonly settingsService = inject(SecretDetectionSettingsService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
|
||||
@Input() settings: SecretAlertSettings = { enabled: false, destinations: [] };
|
||||
@Input() settings: SecretAlertSettings = {
|
||||
enabled: false,
|
||||
destinations: [],
|
||||
minimumAlertSeverity: 'High',
|
||||
maxAlertsPerScan: 100,
|
||||
deduplicationWindowHours: 24,
|
||||
includeFilePath: true,
|
||||
includeMaskedValue: false,
|
||||
includeImageRef: true,
|
||||
};
|
||||
@Input() tenantId = '';
|
||||
@Output() settingsChange = new EventEmitter<SecretAlertSettings>();
|
||||
|
||||
@@ -421,7 +430,7 @@ export class AlertDestinationConfigComponent {
|
||||
this.emitChange();
|
||||
}
|
||||
|
||||
onMinSeverityChange(severity: string): void {
|
||||
onMinSeverityChange(severity: SecretSeverity): void {
|
||||
this.settings = { ...this.settings, minimumSeverity: severity };
|
||||
this.emitChange();
|
||||
}
|
||||
@@ -448,6 +457,8 @@ export class AlertDestinationConfigComponent {
|
||||
const newDest: SecretAlertDestination = {
|
||||
id: crypto.randomUUID(),
|
||||
name: '',
|
||||
channelType: 'Webhook',
|
||||
channelId: '',
|
||||
type: 'webhook',
|
||||
enabled: true,
|
||||
config: {},
|
||||
@@ -466,7 +477,7 @@ export class AlertDestinationConfigComponent {
|
||||
this.updateDestination(index, { name: input.value });
|
||||
}
|
||||
|
||||
onDestTypeChange(index: number, type: string): void {
|
||||
onDestTypeChange(index: number, type: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty'): void {
|
||||
this.updateDestination(index, { type, config: {} });
|
||||
}
|
||||
|
||||
@@ -478,7 +489,7 @@ export class AlertDestinationConfigComponent {
|
||||
});
|
||||
}
|
||||
|
||||
onDestSeverityChange(index: number, severities: string[]): void {
|
||||
onDestSeverityChange(index: number, severities: SecretSeverity[]): void {
|
||||
this.updateDestination(index, { severityFilter: severities });
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
|
||||
import { SecretExceptionService } from '../../services';
|
||||
import { SecretExceptionPattern } from '../../models';
|
||||
import { SecretExceptionPattern, CreateExceptionRequest } from '../../models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-exception-manager',
|
||||
@@ -398,7 +398,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
}
|
||||
|
||||
loadExceptions(): void {
|
||||
this.exceptionService.loadExceptions(this.tenantId).subscribe();
|
||||
this.exceptionService.listExceptions(this.tenantId).subscribe();
|
||||
}
|
||||
|
||||
openCreateDialog(): void {
|
||||
@@ -436,17 +436,18 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
const formValue = this.exceptionForm.value;
|
||||
const existing = this.editingException();
|
||||
|
||||
const payload: Partial<SecretExceptionPattern> = {
|
||||
name: formValue.name,
|
||||
const payload: CreateExceptionRequest = {
|
||||
name: formValue.name ?? '',
|
||||
pattern: formValue.pattern ?? '',
|
||||
reason: formValue.description ?? 'Exception created via UI',
|
||||
matchType: formValue.matchType ?? 'literal',
|
||||
target: formValue.target ?? 'value',
|
||||
enabled: formValue.enabled ?? true,
|
||||
description: formValue.description || undefined,
|
||||
matchType: formValue.matchType,
|
||||
pattern: formValue.pattern,
|
||||
target: formValue.target,
|
||||
enabled: formValue.enabled,
|
||||
};
|
||||
|
||||
const operation = existing
|
||||
? this.exceptionService.updateException(existing.id, payload)
|
||||
? this.exceptionService.updateException(this.tenantId, existing.id, payload)
|
||||
: this.exceptionService.createException(this.tenantId, payload);
|
||||
|
||||
operation.subscribe({
|
||||
@@ -464,7 +465,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
}
|
||||
|
||||
toggleEnabled(exception: SecretExceptionPattern): void {
|
||||
this.exceptionService.updateException(exception.id, {
|
||||
this.exceptionService.updateException(this.tenantId, exception.id, {
|
||||
enabled: !exception.enabled,
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
@@ -477,7 +478,7 @@ export class ExceptionManagerComponent implements OnInit {
|
||||
deleteException(exception: SecretExceptionPattern): void {
|
||||
if (!confirm(`Delete exception "${exception.name}"?`)) return;
|
||||
|
||||
this.exceptionService.deleteException(exception.id).subscribe({
|
||||
this.exceptionService.deleteException(this.tenantId, exception.id).subscribe({
|
||||
next: () => {
|
||||
this.showSuccess('Exception deleted');
|
||||
this.loadExceptions();
|
||||
|
||||
@@ -121,6 +121,7 @@ export class MaskedValueDisplayComponent {
|
||||
|
||||
@Input() value = '';
|
||||
@Input() findingId = '';
|
||||
@Input() tenantId = '';
|
||||
@Input() canReveal = false;
|
||||
|
||||
@Output() revealed = new EventEmitter<string>();
|
||||
@@ -138,12 +139,12 @@ export class MaskedValueDisplayComponent {
|
||||
};
|
||||
|
||||
reveal(): void {
|
||||
if (!this.canReveal || !this.findingId) return;
|
||||
if (!this.canReveal || !this.findingId || !this.tenantId) return;
|
||||
|
||||
this.revealing.set(true);
|
||||
|
||||
this.findingsService.revealValue(this.findingId).subscribe({
|
||||
next: (value) => {
|
||||
this.findingsService.revealValue(this.tenantId, this.findingId).subscribe({
|
||||
next: (value: string) => {
|
||||
this.revealedValue.set(value);
|
||||
this.isRevealed.set(true);
|
||||
this.revealing.set(false);
|
||||
|
||||
@@ -26,8 +26,8 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
|
||||
import { SecretFindingsService } from '../../services';
|
||||
import { SecretFinding } from '../../models';
|
||||
import { MaskedValueDisplayComponent } from './masked-value-display.component';
|
||||
import { SecretFinding, SecretSeverity, SecretFindingStatus } from '../../models';
|
||||
import { MaskedValueDisplayComponent } from './';
|
||||
|
||||
@Component({
|
||||
selector: 'app-secret-findings-list',
|
||||
@@ -426,7 +426,9 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
|
||||
// Expose service signals
|
||||
readonly findings = this.findingsService.findings;
|
||||
readonly pagination = this.findingsService.pagination;
|
||||
readonly page = this.findingsService.page;
|
||||
readonly pageSize = this.findingsService.pageSize;
|
||||
readonly total = this.findingsService.total;
|
||||
readonly loading = this.findingsService.loading;
|
||||
readonly error = this.findingsService.error;
|
||||
|
||||
@@ -459,14 +461,11 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
reload(): void {
|
||||
const tid = this.tenantId();
|
||||
if (tid) {
|
||||
this.findingsService.loadFindings(tid, {
|
||||
this.findingsService.listFindings(tid, {
|
||||
page: 1,
|
||||
pageSize: 25,
|
||||
search: this.searchQuery(),
|
||||
severity: this.severityFilter(),
|
||||
status: this.statusFilter(),
|
||||
sortBy: this.sortField(),
|
||||
sortDirection: this.sortDirection(),
|
||||
severity: this.severityFilter() as SecretSeverity[],
|
||||
status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined,
|
||||
}).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -496,14 +495,11 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
onPageChange(event: PageEvent): void {
|
||||
const tid = this.tenantId();
|
||||
if (tid) {
|
||||
this.findingsService.loadFindings(tid, {
|
||||
this.findingsService.listFindings(tid, {
|
||||
page: event.pageIndex + 1,
|
||||
pageSize: event.pageSize,
|
||||
search: this.searchQuery(),
|
||||
severity: this.severityFilter(),
|
||||
status: this.statusFilter(),
|
||||
sortBy: this.sortField(),
|
||||
sortDirection: this.sortDirection(),
|
||||
severity: this.severityFilter() as SecretSeverity[],
|
||||
status: this.statusFilter() ? [this.statusFilter() as SecretFindingStatus] : undefined,
|
||||
}).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -527,7 +523,8 @@ export class SecretFindingsListComponent implements OnInit {
|
||||
}
|
||||
|
||||
markResolved(finding: SecretFinding): void {
|
||||
this.findingsService.updateStatus(finding.id, 'resolved').subscribe({
|
||||
const tid = this.tenantId();
|
||||
this.findingsService.updateStatus(tid, finding.id, 'Resolved').subscribe({
|
||||
next: () => this.reload(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -260,7 +260,14 @@ type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted';
|
||||
`]
|
||||
})
|
||||
export class RevelationPolicySelectorComponent {
|
||||
@Input() config: RevelationPolicyConfig = { mode: 'masked' };
|
||||
@Input() config: RevelationPolicyConfig = {
|
||||
defaultPolicy: 'FullMask',
|
||||
exportPolicy: 'FullMask',
|
||||
logPolicy: 'FullMask',
|
||||
fullRevealRoles: [],
|
||||
partialRevealChars: 4,
|
||||
mode: 'masked',
|
||||
};
|
||||
@Output() configChange = new EventEmitter<RevelationPolicyConfig>();
|
||||
|
||||
private readonly sampleSecret = 'ghp_abc123XYZ789secret';
|
||||
|
||||
@@ -19,10 +19,10 @@ import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { SecretDetectionSettingsService } from '../../services';
|
||||
import { RevelationPolicySelectorComponent } from '../settings/revelation-policy-selector.component';
|
||||
import { RuleCategoryTogglesComponent } from '../settings/rule-category-toggles.component';
|
||||
import { ExceptionManagerComponent } from '../exceptions/exception-manager.component';
|
||||
import { AlertDestinationConfigComponent } from '../alerts/alert-destination-config.component';
|
||||
import { RevelationPolicySelectorComponent } from './revelation-policy-selector.component';
|
||||
import { RuleCategoryTogglesComponent } from './rule-category-toggles.component';
|
||||
import { ExceptionManagerComponent } from '../exceptions';
|
||||
import { AlertDestinationConfigComponent } from '../alerts';
|
||||
import { RevelationPolicyConfig, SecretAlertSettings } from '../../models';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -24,6 +24,11 @@ export interface SecretDetectionSettings {
|
||||
*/
|
||||
export type SecretRevelationPolicy = 'FullMask' | 'PartialReveal' | 'FullReveal';
|
||||
|
||||
/**
|
||||
* Revelation mode for display settings.
|
||||
*/
|
||||
export type RevelationMode = 'masked' | 'partial' | 'full' | 'redacted';
|
||||
|
||||
/**
|
||||
* Configuration for revelation policies by context.
|
||||
*/
|
||||
@@ -33,6 +38,18 @@ export interface RevelationPolicyConfig {
|
||||
logPolicy: SecretRevelationPolicy;
|
||||
fullRevealRoles: string[];
|
||||
partialRevealChars: number;
|
||||
/** Display mode for UI presentation */
|
||||
mode?: RevelationMode;
|
||||
/** Character used for masking */
|
||||
maskChar?: string;
|
||||
/** Length of the mask */
|
||||
maskLength?: number;
|
||||
/** Number of characters to reveal at start */
|
||||
revealFirst?: number;
|
||||
/** Number of characters to reveal at end */
|
||||
revealLast?: number;
|
||||
/** Required permission for full reveal */
|
||||
requiredPermission?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +64,16 @@ export interface SecretExceptionPattern {
|
||||
expiresAt?: string;
|
||||
ruleIds?: string[];
|
||||
pathFilter?: string;
|
||||
/** Display name for the exception */
|
||||
name?: string;
|
||||
/** Description of why exception exists */
|
||||
description?: string;
|
||||
/** How the pattern is matched */
|
||||
matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix';
|
||||
/** What the pattern applies to */
|
||||
target?: 'value' | 'path' | 'filename';
|
||||
/** Whether the exception is active */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +89,12 @@ export interface SecretAlertSettings {
|
||||
includeMaskedValue: boolean;
|
||||
includeImageRef: boolean;
|
||||
alertMessagePrefix?: string;
|
||||
/** Minimum severity to trigger alert (UI variant) */
|
||||
minimumSeverity?: SecretSeverity;
|
||||
/** Rate limit per hour */
|
||||
rateLimitPerHour?: number;
|
||||
/** Deduplication window in minutes (UI variant) */
|
||||
deduplicationWindowMinutes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +107,12 @@ export interface SecretAlertDestination {
|
||||
channelId: string;
|
||||
severityFilter?: SecretSeverity[];
|
||||
ruleCategoryFilter?: string[];
|
||||
/** Destination type (UI variant) */
|
||||
type?: 'webhook' | 'slack' | 'email' | 'teams' | 'pagerduty';
|
||||
/** Configuration options */
|
||||
config?: Record<string, unknown>;
|
||||
/** Whether destination is enabled */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +159,8 @@ export interface SecretRuleCategory {
|
||||
description: string;
|
||||
ruleCount: number;
|
||||
enabled: boolean;
|
||||
/** Group for categorization */
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +172,16 @@ export interface CreateExceptionRequest {
|
||||
expiresAt?: string;
|
||||
ruleIds?: string[];
|
||||
pathFilter?: string;
|
||||
/** Display name for the exception */
|
||||
name?: string;
|
||||
/** Description of why exception exists */
|
||||
description?: string;
|
||||
/** How the pattern is matched */
|
||||
matchType?: 'literal' | 'regex' | 'glob' | 'prefix' | 'suffix';
|
||||
/** What the pattern applies to */
|
||||
target?: 'value' | 'path' | 'filename';
|
||||
/** Whether the exception is active */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {
|
||||
SecretExceptionPattern,
|
||||
@@ -47,12 +47,10 @@ export class SecretExceptionService {
|
||||
catchError(err => {
|
||||
this._error.set(err.message || 'Failed to load exceptions');
|
||||
this._loading.set(false);
|
||||
return of({ items: [] });
|
||||
return of({ items: [] as SecretExceptionPattern[] });
|
||||
}),
|
||||
// Transform response
|
||||
tap(() => {}),
|
||||
// Return just the items array
|
||||
tap(response => this._exceptions.set(response.items))
|
||||
// Map to array
|
||||
map(response => response.items)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Injectable, inject, signal, computed } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
import { catchError, tap, map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import {
|
||||
SecretFinding,
|
||||
@@ -157,6 +157,21 @@ export class SecretFindingsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveals the secret value for a finding (requires appropriate permissions).
|
||||
*/
|
||||
revealValue(tenantId: string, findingId: string): Observable<string> {
|
||||
return this.http.get<{ value: string }>(
|
||||
`${this.baseUrl}/${tenantId}/${findingId}/reveal`
|
||||
).pipe(
|
||||
map(response => response.value),
|
||||
catchError(err => {
|
||||
this._error.set(err.message || 'Failed to reveal secret value');
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a finding for detail view.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user