Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Failing after 35s
CI / test-python-backend-compliance (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
Nach Abschluss von Profil + Scope werden jetzt automatisch die anwendbaren Regulierungen (DSGVO, NIS2, AI Act, DORA) ermittelt und die zustaendigen Aufsichtsbehoerden (Landes-DSB, BSI, BaFin) aus Bundesland + Branche abgeleitet. - Neues scope-to-facts.ts: Mapping CompanyProfile+Scope → Go SDK Payload - Neues supervisory-authority-resolver.ts: 16 Landes-DSB + nationale Behoerden - ScopeDecisionTab: Regulierungs-Report mit Aufsichtsbehoerden-Karten - Obligations-Seite: Echte Daten statt Dummy in handleAutoProfiling() - Neue Types: ApplicableRegulation, RegulationAssessmentResult, SupervisoryAuthorityInfo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
221 lines
9.5 KiB
TypeScript
221 lines
9.5 KiB
TypeScript
/**
|
|
* Scope-to-Facts Mapper
|
|
*
|
|
* Konvertiert CompanyProfile + ScopeProfilingAnswer[] + ScopeDecision
|
|
* in das Go AI SDK ScopeDecision-Format fuer POST /assess-from-scope.
|
|
*/
|
|
|
|
import type { CompanyProfile } from './types'
|
|
import type { ScopeProfilingAnswer, ScopeDecision } from './compliance-scope-types'
|
|
|
|
/**
|
|
* Payload-Format fuer den Go AI SDK /assess-from-scope Endpoint.
|
|
* Muss mit ucca.ScopeDecision in scope_facts_mapper.go uebereinstimmen.
|
|
*/
|
|
export interface ScopeDecisionPayload {
|
|
employee_count: number
|
|
annual_revenue: number
|
|
country: string
|
|
industry: string
|
|
legal_form: string
|
|
processes_personal_data: boolean
|
|
is_controller: boolean
|
|
is_processor: boolean
|
|
data_art9: boolean
|
|
data_minors: boolean
|
|
large_scale: boolean
|
|
systematic_monitoring: boolean
|
|
cross_border_transfer: boolean
|
|
uses_processors: boolean
|
|
automated_decisions: boolean
|
|
processes_employee_data: boolean
|
|
processes_health_data: boolean
|
|
processes_financial_data: boolean
|
|
uses_cookies: boolean
|
|
uses_tracking: boolean
|
|
uses_video_surveillance: boolean
|
|
operates_platform: boolean
|
|
platform_user_count: number
|
|
proc_ai_usage: boolean
|
|
is_ai_provider: boolean
|
|
is_ai_deployer: boolean
|
|
high_risk_ai: boolean
|
|
limited_risk_ai: boolean
|
|
sector: string
|
|
special_services: string[]
|
|
is_kritis: boolean
|
|
is_financial_institution: boolean
|
|
determined_level: string
|
|
triggered_rules: string[]
|
|
required_documents: string[]
|
|
cert_target: string
|
|
}
|
|
|
|
/**
|
|
* Konvertiert CompanyProfile + Scope-Daten in das Go SDK Payload-Format.
|
|
*/
|
|
export function buildAssessmentPayload(
|
|
profile: CompanyProfile,
|
|
scopeAnswers: ScopeProfilingAnswer[],
|
|
decision: ScopeDecision | null
|
|
): ScopeDecisionPayload {
|
|
const getBool = (questionId: string): boolean => getAnswerBool(scopeAnswers, questionId)
|
|
const getMulti = (questionId: string): string[] => getAnswerMulti(scopeAnswers, questionId)
|
|
|
|
const aiCategories = getMulti('ai_categories')
|
|
const isAIProvider = aiCategories.includes('ai_provider') || aiCategories.includes('ai_developer')
|
|
const isAIDeployer = aiCategories.includes('ai_deployer') || aiCategories.includes('ai_operator')
|
|
const aiRisk = getAnswerString(scopeAnswers, 'ai_risk_assessment')
|
|
|
|
const industry = Array.isArray(profile.industry) ? profile.industry.join(', ') : (profile.industry || '')
|
|
const isFinancial = industry.toLowerCase().includes('finanz') ||
|
|
industry.toLowerCase().includes('bank') ||
|
|
industry.toLowerCase().includes('versicherung') ||
|
|
industry.toLowerCase().includes('financial')
|
|
|
|
return {
|
|
employee_count: parseEmployeeRange(profile.employeeCount),
|
|
annual_revenue: parseRevenueRange(profile.annualRevenue),
|
|
country: profile.headquartersCountry || 'DE',
|
|
industry,
|
|
legal_form: profile.legalForm || '',
|
|
processes_personal_data: true, // Jedes Unternehmen im Tool verarbeitet personenbezogene Daten
|
|
is_controller: profile.isDataController ?? true,
|
|
is_processor: profile.isDataProcessor ?? false,
|
|
data_art9: getBool('data_art9'),
|
|
data_minors: getBool('data_minors'),
|
|
large_scale: parseEmployeeRange(profile.employeeCount) >= 250 || getBool('data_volume'),
|
|
systematic_monitoring: getBool('proc_employee_monitoring') || getBool('proc_video_surveillance'),
|
|
cross_border_transfer: getBool('tech_third_country'),
|
|
uses_processors: getBool('tech_subprocessors'),
|
|
automated_decisions: getBool('proc_adm_scoring'),
|
|
processes_employee_data: getBool('data_hr'),
|
|
processes_health_data: getBool('data_art9'),
|
|
processes_financial_data: getBool('data_financial'),
|
|
uses_cookies: getBool('prod_cookies_consent'),
|
|
uses_tracking: getBool('proc_tracking'),
|
|
uses_video_surveillance: getBool('proc_video_surveillance'),
|
|
operates_platform: (profile.offerings || []).some(o =>
|
|
o === 'software_saas' || o === 'app_web' || o === 'app_mobile'
|
|
),
|
|
platform_user_count: parseCustomerCount(scopeAnswers),
|
|
proc_ai_usage: getBool('ai_uses_ai'),
|
|
is_ai_provider: isAIProvider,
|
|
is_ai_deployer: isAIDeployer,
|
|
high_risk_ai: aiRisk === 'high' || aiRisk === 'unacceptable',
|
|
limited_risk_ai: aiRisk === 'limited',
|
|
sector: mapIndustryToSector(industry),
|
|
special_services: [],
|
|
is_kritis: false, // Kann spaeter aus Branche abgeleitet werden
|
|
is_financial_institution: isFinancial,
|
|
determined_level: decision?.determinedLevel || 'L2',
|
|
triggered_rules: decision?.triggeredHardTriggers?.map(t => t.rule.id) || [],
|
|
required_documents: decision?.requiredDocuments?.map(d => d.documentType) || [],
|
|
cert_target: getAnswerString(scopeAnswers, 'org_cert_target'),
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Hilfsfunktionen
|
|
// =============================================================================
|
|
|
|
/** Liest eine boolean-Antwort aus den Scope-Antworten */
|
|
function getAnswerBool(answers: ScopeProfilingAnswer[], questionId: string): boolean {
|
|
const answer = answers.find(a => a.questionId === questionId)
|
|
if (!answer) return false
|
|
if (typeof answer.value === 'boolean') return answer.value
|
|
if (typeof answer.value === 'string') return answer.value === 'true' || answer.value === 'yes' || answer.value === 'ja'
|
|
if (Array.isArray(answer.value)) return answer.value.length > 0
|
|
return false
|
|
}
|
|
|
|
/** Liest eine Multi-Select-Antwort aus den Scope-Antworten */
|
|
function getAnswerMulti(answers: ScopeProfilingAnswer[], questionId: string): string[] {
|
|
const answer = answers.find(a => a.questionId === questionId)
|
|
if (!answer) return []
|
|
if (Array.isArray(answer.value)) return answer.value as string[]
|
|
if (typeof answer.value === 'string') return [answer.value]
|
|
return []
|
|
}
|
|
|
|
/** Liest einen String-Wert aus den Scope-Antworten */
|
|
function getAnswerString(answers: ScopeProfilingAnswer[], questionId: string): string {
|
|
const answer = answers.find(a => a.questionId === questionId)
|
|
if (!answer) return ''
|
|
if (typeof answer.value === 'string') return answer.value
|
|
if (Array.isArray(answer.value)) return answer.value[0] || ''
|
|
return String(answer.value)
|
|
}
|
|
|
|
/**
|
|
* Konvertiert Mitarbeiter-Range-String in einen numerischen Wert (Mittelwert).
|
|
* "1-9" → 5, "10-49" → 30, "50-249" → 150, "250-999" → 625, "1000+" → 1500
|
|
*/
|
|
export function parseEmployeeRange(range: string | undefined | null): number {
|
|
if (!range) return 10
|
|
const r = range.trim()
|
|
if (r.includes('1000') || r.includes('+')) return 1500
|
|
if (r.includes('250')) return 625
|
|
if (r.includes('50')) return 150
|
|
if (r.includes('10')) return 30
|
|
return 5
|
|
}
|
|
|
|
/**
|
|
* Konvertiert Umsatz-Range-String in einen numerischen Wert.
|
|
* "< 2 Mio" → 1000000, "2-10 Mio" → 6000000, "10-50 Mio" → 30000000, "> 50 Mio" → 75000000
|
|
*/
|
|
export function parseRevenueRange(range: string | undefined | null): number {
|
|
if (!range) return 1000000
|
|
const r = range.trim().toLowerCase()
|
|
if (r.includes('> 50') || r.includes('>50')) return 75000000
|
|
if (r.includes('10-50') || r.includes('10 -')) return 30000000
|
|
if (r.includes('2-10') || r.includes('2 -')) return 6000000
|
|
return 1000000
|
|
}
|
|
|
|
/** Liest die Kundenzahl aus den Scope-Antworten */
|
|
function parseCustomerCount(answers: ScopeProfilingAnswer[]): number {
|
|
const answer = answers.find(a => a.questionId === 'org_customer_count')
|
|
if (!answer) return 0
|
|
if (typeof answer.value === 'number') return answer.value
|
|
const str = String(answer.value)
|
|
const match = str.match(/\d+/)
|
|
return match ? parseInt(match[0], 10) : 0
|
|
}
|
|
|
|
/**
|
|
* Mappt deutsche Branchenbezeichnungen auf Go SDK Sektor-IDs.
|
|
* Diese Sektoren werden fuer NIS2 Annex I/II Pruefung verwendet.
|
|
*/
|
|
function mapIndustryToSector(industry: string): string {
|
|
const lower = industry.toLowerCase()
|
|
|
|
// NIS2 Annex I — "Essential" Sektoren
|
|
if (lower.includes('energie') || lower.includes('energy')) return 'energy'
|
|
if (lower.includes('transport') || lower.includes('logistik')) return 'transport'
|
|
if (lower.includes('bank') || lower.includes('finanz') || lower.includes('financial')) return 'banking'
|
|
if (lower.includes('versicherung') || lower.includes('insurance')) return 'financial_market'
|
|
if (lower.includes('gesundheit') || lower.includes('health') || lower.includes('pharma')) return 'health'
|
|
if (lower.includes('wasser') || lower.includes('water')) return 'drinking_water'
|
|
if (lower.includes('digital') || lower.includes('cloud') || lower.includes('hosting') || lower.includes('rechenzent')) return 'digital_infrastructure'
|
|
if (lower.includes('telekommunikation') || lower.includes('telecom')) return 'digital_infrastructure'
|
|
|
|
// NIS2 Annex II — "Important" Sektoren
|
|
if (lower.includes('it') || lower.includes('software') || lower.includes('technologie')) return 'ict_service_management'
|
|
if (lower.includes('lebensmittel') || lower.includes('food')) return 'food'
|
|
if (lower.includes('chemie') || lower.includes('chemical')) return 'chemicals'
|
|
if (lower.includes('forschung') || lower.includes('research')) return 'research'
|
|
if (lower.includes('post') || lower.includes('kurier')) return 'postal'
|
|
if (lower.includes('abfall') || lower.includes('waste')) return 'waste_management'
|
|
if (lower.includes('fertigung') || lower.includes('manufactur') || lower.includes('produktion')) return 'manufacturing'
|
|
|
|
// Fallback
|
|
if (lower.includes('handel') || lower.includes('retail') || lower.includes('e-commerce')) return 'digital_providers'
|
|
if (lower.includes('beratung') || lower.includes('consulting')) return 'ict_service_management'
|
|
if (lower.includes('bildung') || lower.includes('education')) return 'research'
|
|
if (lower.includes('medien') || lower.includes('media')) return 'digital_providers'
|
|
|
|
return 'other'
|
|
}
|