feat: M18b Filesystem Certificate Discovery — agent scanning, server dedup, triage API

Agent-side:
- Filesystem scanner walks configured directories (CERTCTL_DISCOVERY_DIRS)
- Parses PEM (.pem, .crt, .cer, .cert) and DER (.der) certificate files
- Extracts CN, SANs, serial, issuer/subject DN, validity, key info, SHA-256 fingerprint
- Reports discoveries to control plane on startup + every 6 hours
- Skips files >1MB and private key files

Server-side:
- Migration 000006: discovered_certificates + discovery_scans tables
- Domain model: DiscoveredCertificate, DiscoveryScan, DiscoveryReport
- Three triage states: Unmanaged, Managed (claimed), Dismissed
- Repository with upsert dedup (fingerprint + agent + path)
- Service layer: process reports, claim, dismiss, list, summary
- 7 new API endpoints (84 total):
  POST /agents/{id}/discoveries, GET /discovered-certificates,
  GET /discovered-certificates/{id}, POST .../claim, POST .../dismiss,
  GET /discovery-scans, GET /discovery-summary
- Audit trail: scan_completed, cert_claimed, cert_dismissed events

Tests: 28 new test functions (domain, handler, service layers)
Docs: README, quickstart, demo-guide, demo-advanced, architecture,
      concepts, connectors, features.md all updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shankar0123
2026-03-24 00:25:00 -04:00
parent 8768a7b3ef
commit 667a30870d
23 changed files with 2916 additions and 24 deletions
+37
View File
@@ -79,6 +79,7 @@ func TestCertificateLifecycle(t *testing.T) {
statsHandler := handler.NewStatsHandler(&mockStatsService{})
metricsHandler := handler.NewMetricsHandler(&mockStatsService{}, time.Now())
healthHandler := handler.NewHealthHandler("none")
discoveryHandler := handler.NewDiscoveryHandler(&mockDiscoveryService{})
// Create router and register handlers
r := router.New()
@@ -98,6 +99,7 @@ func TestCertificateLifecycle(t *testing.T) {
statsHandler,
metricsHandler,
healthHandler,
discoveryHandler,
)
// Create test server
@@ -1137,3 +1139,38 @@ func (m *mockStatsService) GetJobStats(ctx context.Context, days int) (interface
func (m *mockStatsService) GetIssuanceRate(ctx context.Context, days int) (interface{}, error) {
return []interface{}{}, nil
}
// mockDiscoveryService implements handler.DiscoveryService for integration tests.
type mockDiscoveryService struct{}
func (m *mockDiscoveryService) ProcessDiscoveryReport(ctx context.Context, report *domain.DiscoveryReport) (*domain.DiscoveryScan, error) {
return &domain.DiscoveryScan{ID: "dscan-test"}, nil
}
func (m *mockDiscoveryService) ListDiscovered(ctx context.Context, agentID, status string, page, perPage int) ([]*domain.DiscoveredCertificate, int, error) {
return nil, 0, nil
}
func (m *mockDiscoveryService) GetDiscovered(ctx context.Context, id string) (*domain.DiscoveredCertificate, error) {
return nil, fmt.Errorf("not found")
}
func (m *mockDiscoveryService) ClaimDiscovered(ctx context.Context, id string, managedCertID string) error {
return nil
}
func (m *mockDiscoveryService) DismissDiscovered(ctx context.Context, id string) error {
return nil
}
func (m *mockDiscoveryService) ListScans(ctx context.Context, agentID string, page, perPage int) ([]*domain.DiscoveryScan, int, error) {
return nil, 0, nil
}
func (m *mockDiscoveryService) GetScan(ctx context.Context, id string) (*domain.DiscoveryScan, error) {
return nil, fmt.Errorf("not found")
}
func (m *mockDiscoveryService) GetDiscoverySummary(ctx context.Context) (map[string]int, error) {
return map[string]int{}, nil
}
+2
View File
@@ -72,6 +72,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *mockCertificateRepository
statsHandler := handler.NewStatsHandler(&mockStatsService{})
metricsHandler := handler.NewMetricsHandler(&mockStatsService{}, time.Now())
healthHandler := handler.NewHealthHandler("none")
discoveryHandler := handler.NewDiscoveryHandler(&mockDiscoveryService{})
r := router.New()
r.RegisterHandlers(
@@ -90,6 +91,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *mockCertificateRepository
statsHandler,
metricsHandler,
healthHandler,
discoveryHandler,
)
server := httptest.NewServer(r)