Neuer Stand Assessor #1
38
AGENTS.md
38
AGENTS.md
|
|
@ -14,6 +14,7 @@
|
|||
- Keep format parsing, structural analysis, semantic mapping, normalization, comparison, persistence, and presentation separated.
|
||||
- Preserve original source values and provenance; normalized values must not destroy source data.
|
||||
- Known tool profiles must extend the generic import pipeline rather than bypass it.
|
||||
- Every new or extended scanner profile must first check its supported report formats for a usable native overall score. Declare `securityScoring` in the profile with exact mapped properties, overall scope, scale, direction and result-population semantics, or an explicit reason why no usable native score is available. Native values take priority over calculated pass rates through the shared scoring engine; never infer overall-score semantics from an arbitrary field named score, a category score or a finding weight. Preserve source values and file/run provenance, version extraction changes and verify native priority, missing/invalid/conflicting values and safe fallback with synthetic fixtures. See `docs/SCORING_MODEL.md`.
|
||||
- React components must not contain source-specific parsing or direct database logic.
|
||||
- Treat Tauri as the current local shell and a possible optional future shell. Do not make new domain logic depend on Tauri when a platform-neutral boundary is possible.
|
||||
- The target deployment is an internal browser application with a backend and PostgreSQL. The browser must not access the database directly.
|
||||
|
|
@ -63,14 +64,51 @@
|
|||
- Place the primary numeric value itself on the exact visual center of the ring. Append a percent sign or other short unit to its right on the same baseline in the smaller shared UI size without letting that unit shift the number away from the center. Supporting counts may remain as subdued secondary text inside the ring, but every text element must stay fully within the clear inner area and must never touch or overlap the ring at any supported value width.
|
||||
- Multi-segment distributions retain their categorical segments, but use the same track, stroke proportion, start position, centered value treatment, and shared tokens as single-value donuts.
|
||||
|
||||
## UI Client Info dashboard
|
||||
|
||||
- The loaded Client Info view is an explicitly scoped exception to the generic tile composition: `.client-command-center` uses the connected command-center treatment and dedicated accent roles defined by `--client-info-*`, while both its outer page background and every dashboard tile inherit the active theme exactly like Work via `--surface-app`, `--tile-surface`, `--tile-surface-hover`, and the corresponding shared text/line roles. This exception supersedes generic card composition only inside that root; it must never leak into Data Hub, Work, Result DB, Pathfinder, dialogs, or the application shell.
|
||||
- Treat Client Info as one connected executive security-posture canvas, not as a collection of unrelated report cards or a spreadsheet. Its stable reading order is environment facts, overall posture, security domains, priority findings and assessment gaps, then source evidence.
|
||||
- Keep environment facts in one compact, single-row evidence rail of icon-led dark tiles. They may be visually separated like the approved Client Info reference, but must remain substantially smaller than the posture and action areas below.
|
||||
- Present Overall Score and Security Domains together on one shared posture surface. Overall Score is the dominant visual anchor; domain scores form a visibly connected branch or spine beside it and must not look like a conventional data table.
|
||||
- Keep Priority Findings and Assessment Gaps in a dedicated action rail. Use status color only for finding markers, score arcs, score nodes, and progress bars; avoid large colored fills and nested card stacks.
|
||||
- Source Overview is the final evidence rail. Explicit user decision 2026-09-10: show the actual versioned ASSESSOR score contribution as the primary number, using a run/file-bound native score when supported and a pass-rate fallback otherwise. Label the method; PingCastle uses the explicit `100 - native risk` convention and retains the raw risk as secondary context. Never insert a raw lower-is-better risk value directly into the overall scale. Follow `docs/SCORING_MODEL.md`; Security Domain values are pass rates, not a decomposition of the mixed-method index.
|
||||
- Unknown or unavailable facts render as an em dash and never as an inferred zero. Keep the dashboard English-only and omit explanatory prose that does not support an immediate first-meeting statement or action.
|
||||
- The Client Info composition still obeys the shared Inter typography roles, donut geometry, marking semantics, and panel inset. Its dedicated palette, connected geometry, and compact briefing surfaces are isolated Client Info primitives rather than application-wide tile replacements.
|
||||
|
||||
## UI branding position
|
||||
|
||||
- Use the Data Hub header as the binding position reference for the ASSESSOR logo, wordmark, and version.
|
||||
- Keep the brand container geometry identical across every application view; do not add area- or view-specific brand padding, margin, alignment, height, or transforms.
|
||||
- Switching views must never move the logo, wordmark, or version.
|
||||
|
||||
## Navigation and rendering performance
|
||||
|
||||
- Treat area switches as a performance-sensitive path. Measure the first visit and repeated visits separately after the workspace has loaded; distinguish main-thread rendering from asynchronous API readiness. Use the local procedure in `docs/architecture/navigation-performance.md`.
|
||||
- Share expensive filtering, sorting and facet counts between their consumers. Component-local `useMemo` does not survive an area unmount. Use bounded, dataset/sheet-owned caches with immutable input identities and all semantic dependencies (profile, dataset, reviews, filters, search, sort and timeslot/path scope); never cache solely by a reusable entity ID. Verify invalidation as well as reuse.
|
||||
- Build raw text, detailed result projections, graph layouts and other large derived content only when the corresponding view needs it. Virtualized views must not serialize or derive presentation fields for every offscreen record. The initial render must also be bounded by the viewport plus overscan.
|
||||
- Large native selectors must not mount thousands of unused options during area navigation. Show the current selection immediately and prepare the complete option list before pointer opening or keyboard focus; verify keyboard access and selection preservation. Construct export payloads only after the export action.
|
||||
- Classify shared headers once per derivation, normalize sort keys once per record, and count facets in a shared pass. Rejected numbered column groups must not trigger repeated scans of all their siblings. Stop category eligibility checks once their cardinality limit is exceeded, while preserving exact counts for eligible facets.
|
||||
- Retained views mount on their first activation. While hidden, freeze their input context and stop geometry observers and animation frames; resume with current inputs when activated. Reset retained state when the workspace scope changes. Keep selections and original source values intact.
|
||||
- Polling an unchanged health/status value must return the existing state instead of rerendering the application. Do not introduce keep-alive polling or data caches without explicit freshness and invalidation behavior.
|
||||
- For performance fixes, add behavioral cost checks (for example, zero source-field reads on an unchanged repeat visit, bounded mounted rows, and fresh results after changed inputs). Record wall-clock measurements as observations rather than fragile CI assertions; run browser comparisons without simultaneous builds or load tests.
|
||||
- Follow the privacy-safe testing rules below. Local opt-in measurements may contain only area names, durations, counters and technical states; no report text, tenant identifiers, filenames, external telemetry or production instrumentation.
|
||||
|
||||
## Security and verification
|
||||
|
||||
- HTML attribute readers must advance monotonically across boolean, quoted, unquoted, malformed, and long valueless attributes; do not use unanchored `name=value` searches that retry every suffix. Resolve repeated internal IDs/names through an import-local index tied to the exact protected markup, preserving duplicate diagnostics, target order, and source offsets. Guard character work and complete source coverage with synthetic regressions; validate timeout fixes in the built isolated worker without raising its time or memory limits.
|
||||
- Context-only extractors must not consume finding tables elsewhere in the same workbook. Compose them with the generic table extractor, keep original table indices, and verify summary, findings, and supporting sheets together through canonical projection and result linking.
|
||||
- Embedded scanner globals must be located lexically outside strings, comments, HTML attributes, and other raw-text containers. Missing IDs, unreadable payloads, conflicting assignments, repeated IDs, and declared-count mismatches require explicit diagnostics. A declared empty report is distinct from an unreadable nonempty report; explicit null result tables with zero results are valid.
|
||||
- Keep native detail timestamps at their full precision and preserve exact source values and field paths, including explicit nulls and unknown fields. Derived display fields reference their real input objects; never invent native paths for generated compatibility columns. Test source-path resolution and detail-cell equality, not only row counts.
|
||||
- Scanner coverage must be verified against independently counted source records and source status totals, not just a successful parser return. Preserve declared totals and emit a diagnostic when they disagree with individual results; never drop records to make totals agree.
|
||||
- Preserve XML attributes as exact name/value pairs with attribute-level source paths, including namespaces, zero, false and empty values. Explicitly empty leaves must remain distinguishable from absent data; never reconstruct attributes from an ambiguous display serialization.
|
||||
- Lexical HTML readers must respect quoted attribute boundaries, raw-text elements and malformed/unquoted attributes without treating tooltip markup as visible tags. Sanitize complete attributes, not attribute-like strings inside other values. Bounded excerpts must not expose a partial trailing tag or discard subsequent source sections. Verify table/section coverage as well as clean cell text in both runtimes.
|
||||
- Missing native rule IDs must fail explicitly. Diagnose repeated IDs even when an established first-presentation policy retains one finding. Compare matched counts by category, not catalog size; preserve the original counters and distinguish a declared empty report from an announced but unreadable result set.
|
||||
- Embedded report data is inert input. Support only an explicitly bounded static data grammar; never evaluate scripts, calls, getters, spreads, computed properties or template substitutions. Unsupported or malformed recognized reports must fail explicitly instead of becoming successful zero-finding imports. Keep production/browser and server behavior aligned.
|
||||
- Mapping rules for ambiguous labels must include their table or candidate context. In Maester, `Result` beside `Status` is evidence; native `Result` without a separate status column is the status. Preserve original values and exact source paths through both variants.
|
||||
- Retain scanner-level metadata separately from findings, including unknown nested fields. Distinguish scanner-runtime machine information from assessed-environment assets. Version extraction/mapping changes and recommend reimport without rewriting historic imports.
|
||||
- Apply `docs/REIMPORT_POLICY.md` to every importer and profile change. Any change to recognition, parsing/extraction coverage, persisted context/evidence, finding identity, mapping, normalization, status/outcome/severity/IOE, completeness diagnostics, or newly captured score inputs must add an append-only processing manifest version and mark affected historical imports for reimport. Presentation-only changes over already retained facts do not require reimport.
|
||||
- A reimport may replace scanner-derived records only. Preserve complete client-owned review objects, including future fields and accepted-risk data, through a unique scanner/rule/asset identity. Keep the prior review and surface a persistent unresolved state when matching is absent or ambiguous; never guess by title or position.
|
||||
|
||||
- Preserve local-only behavior until a server transition is explicitly implemented and documented.
|
||||
- Do not introduce external uploads, telemetry, CDN assets, network APIs, shell access, or broader Tauri capabilities without explicit user approval and a security-model update.
|
||||
- Treat imported HTML, XML, Excel, JSON, and CSV files as untrusted input. Never execute active content from imported files.
|
||||
|
|
|
|||
48
SECURITY.md
48
SECURITY.md
|
|
@ -2,6 +2,15 @@
|
|||
|
||||
## Lokale Datenverarbeitung
|
||||
|
||||
HTML-Parser-Nachhaertung 2026-09-09: Attributnamen und -werte werden linear
|
||||
gelesen, einschliesslich langer wertloser beziehungsweise fehlerhafter Attribute.
|
||||
Dies vermeidet quadratisches Regex-Backtracking vor der eigentlichen Extraktion.
|
||||
PingCastle-Verweisziele werden pro unveraendertem Markup indiziert; doppelte
|
||||
Definitionen bleiben mehrdeutig, Zielreihenfolge und Quellpositionen erhalten.
|
||||
Die Indizes leben nur im aktuellen Import. Workerzeit, Speichergrenzen und
|
||||
Netzwerk-/Persistenzberechtigungen werden nicht erweitert. Der im Screenshot
|
||||
gezeigte Report wurde nicht gelesen; die Reproduktion verwendet synthetische Daten.
|
||||
|
||||
ASSESSOR besitzt zwei kontrollierte lokale Betriebsarten:
|
||||
|
||||
1. Im empfohlenen Browserbetrieb liefert das lokale Node.js-Backend die React-App
|
||||
|
|
@ -22,6 +31,45 @@ Dokumentstruktur bereit. Skriptausführung ist nicht aktiviert; aktive
|
|||
HTML-Elemente werden vor der Tabellenauswertung entfernt. Umfangreiche oder tief verschachtelte HTML-Berichte verwenden automatisch eine begrenzte, nicht rekursive Tabellenextraktion. Excel-Formeln werden
|
||||
nicht ausgewertet; verschlüsselte Archive und eingebettete VBA-Projekte werden abgewiesen. Spreadsheet-orientierte CSV-Exporte neutralisieren formelaktive Zellanfänge.
|
||||
|
||||
### PurpleKnight-Berichte
|
||||
|
||||
Workbook-Zusammenfassungen werden mit der generischen Tabellenextraktion
|
||||
kombiniert, nicht anstelle der Indikatoren verarbeitet. HTML liest ausschließlich
|
||||
inert eingebettete, begrenzte Datenobjekte aus lexikalisch erkannten
|
||||
`window.reportJSON`- und `window["Category_n"]`-Zuweisungen. Zeichenketten,
|
||||
Kommentare, HTML-Attribute und andere Raw-Text-Elemente sind keine Datenquellen.
|
||||
Beschädigte Objekte, fehlende native IDs und konkurrierende Zuweisungen werden
|
||||
abgewiesen. Wiederholte IDs und abweichende native Ergebnis-/Detailzähler
|
||||
erzeugen Warnungen ohne stilles Verwerfen von Vorkommen. Null-Details ohne
|
||||
Ergebnisse und ausdrücklich deklarierte leere Berichte bleiben zulässig.
|
||||
Originalwerte, vollständige Zeitstempel, unbekannte Felder und native Quellpfade
|
||||
bleiben erhalten; Summary und Berichtskontext sind von Findings getrennt.
|
||||
Es entstehen keine neuen Ausführungs-, Netzwerk- oder Datenbankrechte.
|
||||
|
||||
### Strukturtreue bei PingCastle
|
||||
|
||||
XML-Attribute werden als separate Originalwerte mit exakten Quellpfaden geführt;
|
||||
ausdrückliche Leere wird nicht als nicht erhoben interpretiert. Lexikalische
|
||||
HTML-Auswertung respektiert zitierte Attribute und Raw-Text-Grenzen, sodass
|
||||
Tooltip-Markup weder zu scheinbaren Tabellen noch zu Zelltext wird. Aktive
|
||||
Attribute werden nur als vollständige Tag-Attribute entfernt. Fehlende native
|
||||
Rule IDs, Dubletten und abweichende deklarierte Trefferzahlen besitzen explizite
|
||||
Diagnosen; ein angekündigter, aber nicht lesbarer Befundbestand darf nicht als
|
||||
erfolgreicher Nullbefund gelten. Regeln und Originalzähler bleiben getrennt.
|
||||
Diese Änderung öffnet keine aktive HTML-, Netzwerk- oder Persistenzberechtigung.
|
||||
Details und Grenzen: `docs/IMPORT_SECURITY_LIMITS.md`.
|
||||
|
||||
### Eingebettete Maester-Berichtsdaten
|
||||
|
||||
Maester-HTML kann neben JSON ein minifiziertes JavaScript-Objektliteral enthalten.
|
||||
Der Quellenextractor liest daraus ausschließlich statische Daten: Objekte, Arrays,
|
||||
Zeichenketten (auch mehrzeilig ohne Interpolation), endliche Zahlen, Boolean und
|
||||
Null sowie die festen Boolean-Schreibweisen `!0`/`!1`. Er führt keinerlei Code aus.
|
||||
Aufrufe, Variablenzugriffe, Getter, Spread, berechnete Schlüssel, doppelte Schlüssel
|
||||
und Template-Interpolation werden abgewiesen. Die Grenzen und Regressionen sind in
|
||||
`docs/IMPORT_SECURITY_LIMITS.md` beschrieben. Importierte Skripte bleiben inaktiv;
|
||||
es gibt keine neue Netzwerk-, Shell- oder Tauri-Berechtigung.
|
||||
|
||||
### Verweisziele aus Berichten
|
||||
|
||||
Berichte führen fachlich notwendige Belegstellen als Verweise mit (Purple
|
||||
|
|
|
|||
|
|
@ -2,6 +2,82 @@
|
|||
|
||||
Status: lebendes Projektdokument
|
||||
|
||||
Reimport-Schutz 2026-09-10 (Phase 6/8/9/13C mit Phase-7-Hinweis, ausdruecklicher Nutzerauftrag): Verarbeitungsrelevante Aenderungen an Formaterkennung, Parser/Extractor, Scannerprofil, stabiler Identitaet, Mapping, Normalisierung, Status-/Evidenzvertrag oder nativer Scoregewinnung muessen ueber append-only Processing-Manifeste versioniert und fuer bestehende Importe als Reimportbedarf sichtbar werden; reine Darstellungsaenderungen auf bereits gespeicherten Daten benoetigen keinen Reimport. Generische/tabellarische Importe ohne belastbares Profil erscheinen vorsorglich als gelber Pruefstatus. Direkter Reimport und die Uebernahme eines bereits importierten Ersatzlaufs uebertragen kundenbezogene Finding-Reviews atomar in lokaler und PostgreSQL-Persistenz. Die Zuordnung verwendet ausschliesslich die eindeutige stabile Identitaet aus Scannerwerkzeug, nativer Regel-ID und betroffenem Asset; bei fehlender oder mehrdeutiger Zuordnung wird nicht nach Titel oder Position geraten. Der vollstaendige Reviewvertrag wird kopiert und erhaelt dadurch Status, Prioritaet, Action-Plan-Zuordnung, Owner, Termin, Kommentar, Voraussetzungen, Zugriffe, naechsten Schritt, Akzeptanzevidenz, Marker/Accepted Risk sowie spaetere additive JSON-Felder. Bereits vorhandene Angaben am Ersatzlauf haben Vorrang. Nicht eindeutig uebertragbare Angaben bleiben am auditierbar erhaltenen Ursprungslauf gespeichert; Anzahl und dauerhafter `Review required`-Hinweis werden am Ersatzimport persistiert. Original-SHA, Vorgaengerbezug und Uebertragungsherkunft bleiben nachvollziehbar. Regeln und Szenarien stehen in `docs/REIMPORT_POLICY.md`. Verifikation: 1.205 Tests bestanden, acht optionale Tests uebersprungen; Local-only- und Designpruefung erfolgreich. Keine SQL-Migration, Realdateninspektion oder kanonische Datenmutation; Phasenstatus unveraendert.
|
||||
|
||||
Purple-Knight-Scorezuordnung 2026-09-10 (Phase 7/13C, Nutzerkorrektur): Eine native score-only Assessment Summary und die eindeutige Findings-Datei desselben Scanlaufs werden nun ueber Assessment, Werkzeug, Scanlauf und Verarbeitungskontrakt verbunden. Der native Wert bleibt der Scorebeitrag; Passed-/Deviation-Zahlen stammen aus der Findings-Datei. Die Zuordnung scheitert geschlossen bei mehreren nativen Traegern oder mehrdeutigen Ergebnispopulationen. Die synthetische Regression mit nativ 65, 96 Passed und 19 Deviations liefert in der Dashboardprojektion 65 Native score statt Pass rate 83 und behaelt 19 Deviations. 1.199 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designchecks sowie Client-/Serverbuild erfolgreich. Keine Datenmutation, Migration, Realdateninspektion oder Backendneustart; Phasenstatus unveraendert.
|
||||
|
||||
Native Scores fuer weitere Scanner 2026-09-10 (Phase 6/13C mit Phase 7, Nutzerpraezisierung): Jedes deklarative Scannerprofil enthaelt jetzt verpflichtend eine versionierte Scoredefinition mit nativen Gesamtwertfeldern, Skala, Richtung und Fallbackpopulation oder einer begruendeten Nichtverfuegbarkeit. Die gemeinsame Berechnung liest den Profilkatalog statt einer separaten Scannerliste; neue Profile erhalten automatisch den nativen Vorrang. Zehn neue synthetische Regressionen sichern den Profilvertrag und einen zusaetzlichen Scanner einschliesslich Dashboardprojektion, Nullwert, Risikoinversion, fehlerhaften/fremden Werten und Findings-only-Fallback. 1.198 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designchecks und Client-/Serverbuild erfolgreich. Vorhandene Scoremethoden, Importversionen und Daten bleiben unveraendert; Phasenstatus unveraendert.
|
||||
|
||||
Hybridscore 2026-09-10 (Phase 7/13C mit Phase-8-Vergleichsschutz, ausdruecklich freigegeben): Regel 1.0.0 verwendet den exakt datei-/laufgebundenen nativen Purple-Knight-/CIS-CAT-Score beziehungsweise `100 - PingCastle-Risiko`, sonst eine belastbare Bestehensquote. Feste Quellgewichte, verifizierte Lauf-/Hash-Deduplikation, ungerundete Berechnung und gekennzeichnete Datenluecken verhindern doppelte oder still falsch zugeordnete Beitraege. Security Domains heissen Pass rates; Source Overview und der neue Methodikdialog zeigen Beitrag, Verfahren, Gewicht, Pruefbasis und Quellzeit. Neue Assessments speichern die Regel im bestehenden JSON/JSONB-Vertrag; fehlende historische Angaben bedeuten dauerhaft Version 1.0.0. Historische offene Scannerverpflichtungen werden fuer den Scorevergleich read-only mit Herkunft fortgefuehrt, Fortschritt nur bei unveraendertem Scope/Verfahren und aktueller Evidenz berechnet. Das weitergehende persistierte Phase-8-Backlog-/Retirement-Arbeitspaket bleibt offen. Entscheidungsregeln, Herstellerbelege und Grenzen stehen in `docs/SCORING_MODEL.md`. Verifikation: 1.188 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen, Local-only-/Designchecks und Client-/Serverbuild erfolgreich; die vorbestehende TS7016-Blockade wurde durch die fehlende Typdeklaration der synthetischen HTML-Fixture behoben. Keine SQL-Migration, Realdateninspektion, Datenmutation oder Backendneustart; Phasenstatus unveraendert.
|
||||
|
||||
Source-Wechsel ohne Ansichtswechsel 2026-09-10 (Phase 7, Nutzerkorrektur): Der Client-Info-Source-Selektor verwendete den Findings-Drilldown und oeffnete dadurch Work. Die kontrollierte Auswahl setzt jetzt ausschliesslich den gemeinsamen Source-Filter, wechselt bei Bedarf auf das Timeslot-Aggregatblatt und behaelt Bereich sowie Work-Unteransicht. Scan All entfernt den Source-Filter. Score, Domain-/Prioritaetszahlen, Statuszaehler und Source Overview werden innerhalb Client Info aus der gefilterten Ergebnismenge abgeleitet; alle Quelloptionen bleiben erreichbar. Der blattgebundene Cache beruecksichtigt die unveraenderliche Quellenauswahl, damit Wechsel frische Werte liefern und unveraenderte Wiederbesuche keine Quellfelder erneut lesen. Explizite Finding-Drilldowns bleiben Navigation und uebernehmen den gewaehlten Source-Kontext. Fuenf synthetische Interaktionstests decken Client Info, die vier Work-Unteransichten, Scan All, Wiederbesuch und Austausch des Timeslot-Datensatzes ab. Verifikation: 1.157 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Clientbundle erfolgreich. Gesamtbuild weiterhin am vorbestehenden TS7016 fuer html-timeout-fixture.mjs blockiert. Keine API-/Persistenz-/Schemaaenderung, kein Reimport oder Backendneustart; Phasenstatus unveraendert.
|
||||
|
||||
PurpleKnight-Statusabgleich 2026-09-10 (Phase 7, Diagnose mit Bezug zu 13A/13C): Der ausdruecklich auf Statusfelder und Zaehler freigegebene read-only Abgleich zeigt zwei unterschiedliche native Ergebnisstaende in HTML und CSV; die gespeicherten HTML-Originalstatusfelder und Summenzaehler stimmen mit der ASSESSOR-Zaehlung ueberein. Keine Importkorrektur oder Angleichung an fremde Summarywerte. Bestaetigter separater Darstellungsfehler behoben: Client Info erkennt die deklarierten PurpleKnight-Filter Not Relevant als not-applicable sowie Not selected/Canceled als not-run. Nicht anwendbare Kontrollen zaehlen damit nicht als Assessment Gaps oder zum Coverage-Nenner; Passed/Deviation und Originaldaten bleiben unveraendert. Vier neue synthetische Regressionen pruefen Profil-/Mischansicht, echte Luecken, Coverage und unveraenderte Quelldaten; elf fokussierte Dashboardtests und 49 Import-/Status-/Facettentests erfolgreich. Gesamtlauf: 1.152 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung erfolgreich. Clientbundle erfolgreich; Gesamtbuild bleibt am vorbestehenden TS7016 fuer tests/importers/html-timeout-fixture.mjs blockiert. Keine Persistenzmutation, Migration, Importversionierung, Reimport oder Backendneustart; temporaeres Diagnoseskript entfernt. Phasenstatus unveraendert.
|
||||
|
||||
Work-GPO-Statusfilter wiederhergestellt 2026-09-09 (Phase 7/13B, Nutzerkorrektur): Der Versionssprung auf Evaluator 1.4.0 hatte gespeicherte 1.3.3-Vergleiche im Work-Ladepfad ausgeblendet. Work darf diese lesekompatiblen Ergebnisse jetzt ausschliesslich als Anzeigeersatz verwenden, solange fuer dieselbe aktive Aggregation keine aktuelle Evaluation vorliegt. Die Backendfreigabe des Aggregationskontexts und die vollstaendige Beobachtungs-/Timeslotabgrenzung bleiben erhalten; historische Profile/Snapshots werden nicht wieder aktiviert. Aktuelle Engineversion hat Vorrang, innerhalb derselben Version gilt weiterhin vollstaendigste Baselineabdeckung und danach Aktualitaet. Beschreibung und Evaluation-readiness-Feld kennzeichnen Altwerte ausdruecklich als neu auszuwerten. Der Standardselektor fuer aktuelle Result-DB-Projektion und fachliche Verarbeitung bleibt strikt, alte Laeufe gelten nicht als abgeschlossene Neuauswertung. GPO-Statusoptionen bleiben in GPO-/gemischten Workspaces auch bei null Treffern auswaehlbar; andere Facetten behalten ihre bisherige Trefferbegrenzung. Synthetische End-to-End-Regression bestaetigt Laden von Altwerten, Conflicts/Not configured/Duplicates/Passed mit je einem filterbaren Treffer, sichtbare Nulloptionen, Kennzeichnung und unveraenderte Originaldaten. 34 gezielte Tests und Gesamtlauf mit 1.148 Tests erfolgreich, acht optionale Datenbanktests uebersprungen; Local-only-/Designpruefung und Serverbuild erfolgreich. Clientbuild bleibt an der unabhaengigen fehlenden Typdeklaration fuer html-timeout-fixture.mjs blockiert. Keine Datenmutation, Neuauswertung, SQL-/API-Aenderung oder Backendneustart. Fuer die Filterkorrektur genuegt erneutes Oeffnen des Kunden mit aktualisiertem Frontend; die fachliche Neuauswertung bleibt getrennt erforderlich. Phasenstatus unveraendert.
|
||||
|
||||
GPO-Vorlagenkontext 2026-09-09 (Phase 13B, expliziter Nutzerauftrag): Baselineanalyzer 1.3.0 erhaelt Namen jeder enthaltenen GPO-Vorlage aus begrenzten Backup.xml-/bkupInfo.xml-/GPReport.xml-Feldern mitsamt Quellpfad und SHA-256. Der bestehende SAX-Parser liest diese Namen nur per Opt-in; Kundenidentitaeten bleiben unveraendert. Eine enge Namensgrammatik erkennt Windows-Client, Member Server und Domain Controller samt Version; RS1 wird fuer den Vergleich zu 1607 normalisiert, der Originalname bleibt erhalten. Fehlende, unbekannte oder widerspruechliche Namen erzeugen keine geratene Rolle. Evaluator 1.4.0 waehlt Vorlagen nur bei genau einem expliziten, passenden Vergleichskontext ohne Inventarherkunft; unbekannte Vorlagen werden nicht ausgeschlossen. Gleiche Empfehlungen bleiben auch ohne Rollenwahl vergleichbar. Bei unterschiedlichen Empfehlungen ohne Zuordnung bleibt die bisherige Konflikt-/Reviewdarstellung bestehen, jetzt mit ausdruecklicher Erklaerung, dass kein Kunden-GPO-Konflikt bewiesen ist. Kein passender Kontext erzeugt keinen Pass/Fail. Alle Kandidaten und Auswahlreferenzen bleiben als Evidenz erhalten. Die Detailanzeige unterscheidet Paket und Vorlagenname, markiert ausgewaehlte Empfehlungen und fuehrt gleiche Werte verschiedener Vorlagen nicht mehr zusammen. 50 abschliessende synthetische Tests erfolgreich; Gesamtdurchlauf 1.146 Tests erfolgreich, acht optionale Datenbanktests uebersprungen. Local-only-Pruefung semantisch auf Opt-in und unveraenderte technische Identitaet praezisiert; Local-only/Design separat erfolgreich. Serverbuild erfolgreich. Client-Gesamtbuild durch unabhaengige fehlende Typdeklaration in html-attribute-performance.test.ts fuer html-timeout-fixture.mjs blockiert; direkter Bundleversuch durch Sandbox spawn EPERM blockiert. Keine SQL-Migration, Datenmutation, Live-Inhaltspruefung oder Backendneustart. Aktivierung fuer bestehende Daten erfordert Neuanalyse der Original-Baselinepakete und anschliessende neue Evaluation; alte Analysen/Laeufe werden nicht ueberschrieben. Die automatischen Inventarkontexte bleiben bewusst kein Beweis der Kunden-GPO-Anwendbarkeit. Phase 13B bleibt in Arbeit.
|
||||
|
||||
HTML-Timeout-Nachhaertung 2026-09-09 (Phase 13/13A): Der Screenshot meldet einen Parserabbruch nach 30 Sekunden; der betreffende Report wurde nicht gelesen. Ein unabhaengiger synthetischer 3.159.338-Byte-Fall reproduziert denselben Fehler im bisherigen Produktionsworker nach 30.011 ms. Gemeinsamer linearer Attributleser ersetzt unbeschraenkte name=value-Suchwiederholungen; importlokaler Elementindex ersetzt wiederholte Volltextsuchen nach PingCastle-Verweiszielen. Formatadapter HTML 1.1.1, PingCastle-Extractor 2.2.1 und CIS-CAT-Extractor 1.3.1; unveraenderte Profile, Mappingvertraege, Zeit-/Speicherlimits und Persistenz. Neun neue synthetische Regressionen erfolgreich; der gebaute Worker erhaelt alle 40 Regeln und 40 verknuepften Tabellen in beobachteten 1.590 ms. 1.131 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Client-/Serverbuild und separate Designpruefung erfolgreich. npm test endet anschliessend an einem fachfremden GPO-Sicherheitscheck (tests/local-only-check.mjs:211: displayName-Verbot gegen unabhaengig geaenderten gpo-metadata-parser); diese Dateien wurden nicht veraendert. Kanonischer PostgreSQL-Container samt bestehendem Volume und Portbindung geprueft, Backend neu gestartet. Synthetischer Parse-only-HTTP-Abgleich bestaetigt 40 Regeln/Tabellen und unveraenderte Repositoryrevision; App/Backend/Proxy HTTP 200. Temporaere Diagnosepruefungen entfernt, dauerhafter isolierter Lasttest bleibt wiederholbar. Keine Phase wird hierdurch neu abgeschlossen.
|
||||
|
||||
PurpleKnight-Vollstaendigkeit 2026-09-09 (Phase 13A/13C, freigegebener Fix des Audits): Profil 1.3.0, HTML-Extractor 1.2.0 und Summary-Extractor 1.1.0 verarbeiten Workbook-Summary und Findingtabellen gemeinsam ueber die generische Pipeline. HTML liefert getrennten Berichtskontext, exakte Originalwerte/Quellpfade und vollstaendige Detailzeitstempel. Lexikalisch erkannte statische Zuweisungen bleiben inert; defekte Payloads, fehlende IDs und konkurrierende Bindings scheitern explizit, doppelte IDs und deklarierte Zaehlerabweichungen erzeugen Warnungen ohne Datenverlust. Native Null-Details und deklarierte leere Berichte bleiben gueltig. CSV-Indikatorerkennung kombiniert Name und Schema; Dateiname allein reicht nicht. 25 neue synthetische Regressionen, insgesamt 1.122 Tests bestanden und acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung sowie Client-/Serverbuild erfolgreich. Dummy: XLSX/HTML je 119 Vorkommen und 20 verknuepfte Detailtabellen mit 46 Zeilen, XLSX 22 und HTML 45 Kontextbeobachtungen; CSV 119 Vorkommen und alle 19 Details mit 45 Zeilen. 4.743 HTML-Originalfeldwerte samt Pfaden und alle 213 Detailzellen unabhaengig exakt geprueft. Alle 24 Dateien ueber Parse-only-HTTP abgeglichen; abschliessender Drei-Format-Check am finalen Backend bestaetigt die Zahlen und unveraenderte Repositoryrevision. Kanonischer PostgreSQL-Datastore unveraendert, Backend aktualisiert. Keine Schemaaenderung, Migration, Speicherung oder Reimport; alte Importe benoetigen expliziten Reimport. Vorgaben und historischer Pruefbericht aktualisiert, temporaere Auditdateien entfernt. Phase 13A bleibt abgeschlossen, Phase 13C bleibt in Arbeit.
|
||||
|
||||
GPO-Empfehlungsanzeige vereinfacht 2026-09-09 (Phase 7/13B, Nutzerpraezisierung): Die Beschreibung zeigt nur noch Baselinebezeichnung und Sollwert. Gleiche Anzeigeempfehlungen innerhalb desselben Ergebnisses werden zusammengefasst; Source und Backupkennung bleiben in der gespeicherten Evidenz, entfallen aber hier. Die Vollstaendigkeitspruefung zaehlt geladene Referenzen unabhaengig von der Anzeigeverdichtung, damit zusammengefasste Werte nicht faelschlich als fehlend gelten. Keine Aenderung der Konfliktregel oder Zuordnung, keine Migration. 16 gezielte Tests, 1.122 Gesamttests erfolgreich, acht optionale Datenbanktests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Die beim vorherigen Durchlauf beobachteten Gesamtpruefblocker bestehen in diesem Durchlauf nicht mehr. Kein Phasenabschluss.
|
||||
|
||||
GPO-Baselineempfehlungen 2026-09-09 (Phase 7 mit Bezug zu 13B, begrenzter Nutzerauftrag): Bei mehrdeutigen Baselineempfehlungen zeigt die Detailbeschreibung die referenzierten Empfehlungen mit Baselinebezeichnung, Sollwert, Quellpfad und Backupkennung. Die Zuordnung bleibt an die gespeicherte Analyse, Baseline und Einstellungsidentitaet gebunden; unbekannte Zielrollen werden nicht geraten. Explizit leere Kontenlisten und nicht ladbare Referenzen erhalten eigene Erklaerungen. Vorhandene Beschreibungen bleiben erhalten. Bewertung, Zaehler, Persistenz und Importdaten bleiben unveraendert; kein Reimport erforderlich. 16 gezielte synthetische Guidance-/Darstellungstests und Client-/Serverbuild erfolgreich, Designpruefung erfolgreich. Der parallel veraenderte Gesamtstand ist nicht vollstaendig gruen: beobachteter Purple-Knight-Profilversionstest (1.2.0/1.3.0) und Local-only-Pruefung in static-report-literal.ts ausserhalb dieser Aenderung. Kein Phasenabschluss und keine Live-Inhaltspruefung.
|
||||
|
||||
PingCastle-Strukturtreue 2026-09-09 (Phase 13A/13C, freigegebene Auditpunkte 1-3): Profil 1.5.0 / Extractor 2.2.0 erhaelt XML-Attribute und explizite Leere, liest HTML-Tooltipattribute ohne Zelltextverunreinigung und diagnostiziert fehlende/doppelte Rule IDs sowie abweichende Kategorie-Trefferzahlen. HTML/XML-Formatadapter 1.1.0, unveraenderte Mapping-/Normalisierungsvertraege und Finding-Kompatibilitaet. Dummy-Abgleich: je 37 Regeln, XML 1.137 Beobachtungen mit allen 403 Attributwerten und 70 leeren Blaettern; HTML alle 125 Tabellen, 1.513 Beobachtungen, 17 bisherige Abschnitte plus separate Regelbilanz. Keine Attributreste in Kontext, Details oder verknuepften Detailtabellen. 21 neue synthetische Regressionen; 1.096 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Laufendes Backend nach kanonischer Datastorepruefung neu gestartet; Parse-only-HTTP-Abgleich bestaetigt Zahlen und unveraenderte Repositoryrevision, App/Backend/Proxy HTTP 200. Kein Reimport, keine Importpersistenz oder Schemaaenderung. Vorgaben und Sicherheitsdokumentation ergaenzt, temporaere Dummy-Pruefdatei entfernt. Auditpunkte 4-5 (semantischer XML-Kontextvertrag, Control-Path-JSON) bleiben offen; Phase 13A abgeschlossen und Phase 13C in Arbeit bleiben unveraendert.
|
||||
|
||||
Maester-Vollstaendigkeit 2026-09-09 (Phase 13A/13C, ausdruecklicher Nutzerauftrag einschliesslich HTML): Der native HTML-Extractor 1.1.0 liest eingebettete statische JavaScript-Objektliterale ueber eine begrenzte Datengrammatik ohne Codeausfuehrung. Nicht unterstuetzte dynamische Ausdruecke, defekte Testeintraege und konkurrierende Berichtswurzeln scheitern mit einer Importdiagnose statt still null Befunde zu erzeugen. Native Strukturerkennung funktioniert auch hinter dem begrenzten Rohtext-Erkennungsausschnitt. Das Maester-Profil 1.1.0 trennt Excel-Result-Evidenz von einer vorhandenen Statusspalte; natives CSV/HTML-Result bleibt ohne separate Statusspalte der Ergebnisstatus. Berichtskontext, Originalfelder und Herkunft bleiben erhalten; Scanner-Rechner und PowerShell sind explizit Laufzeitkontext, keine bewerteten Assets. Deklarierte Zaehler und aus Einzeltests berechnete Werte bleiben getrennt, Widersprueche erzeugen Warnungen. Am freigegebenen Dummy liefern Excel und HTML jeweils 318 Kandidaten/Vorkommen und 297 Evidenzen; HTML liefert zusaetzlich 39 Umgebungsbeobachtungen und einen Berichtsabschnitt. IDs, Status und Severity stimmen fuer alle 318 Tests zwischen den Formaten ueberein; 6.235 extrahierte HTML-Originalfelder wurden unabhaengig gegen ihre Quellpfade geprueft. Zwei Quellwidersprueche sind belegt: TotalCount 315 statt 318 und FailedCount 67 statt 70. 1.075 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Damit sind auch die oben historisch dokumentierten temporaeren Maester-Test-/Buildblocker aufgeloest; temporaere Pruefdateien entfernt. Das neu gestartete Backend verwendet den verifizierten kanonischen PostgreSQL-Datastore; App, Backend und Proxy antworten mit HTTP 200. Parse-only-HTTP-Pruefung beider Dummy-Dateien bestaetigt Profil 1.1.0, dieselben Zahlen und unveraenderte Repositoryrevision. Keine Schemaaenderung, Migration oder Speicherung von Importdaten; bestehende Maester-Importe benoetigen einen expliziten Reimport. AGENTS.md, SECURITY.md und docs/IMPORT_SECURITY_LIMITS.md halten die Schutz- und Abnahmeregeln fest. Phase 13A bleibt abgeschlossen, Phase 13C bleibt in Arbeit.
|
||||
|
||||
Client-Info Findinggrundlage 2026-09-09 (Phase 7 mit Bezug zu 13B, Nutzerkorrektur): Security Domains und Priority Findings verwenden jetzt dieselbe autoritative, disjunkte Deviation-Grundmenge. Ursache der ueberhoehten Domainzahlen war eine zweite Statusableitung: Abfragen nach in einem Profil nicht definierten Statuswerten wurden vom Profilfallback als Treffer behandelt; dadurch konnten insbesondere GPO-Kontextzeilen als Deviations gelten. Zusaetzlich durfte derselbe Datensatz mehreren Domains angehoeren. Kontextstatus werden nun vor Deviations getrennt, nur deklarierte Statusfilter abgefragt und jeder Datensatz genau einer primaeren Domain zugeordnet; nicht eindeutig klassifizierbare Deviations erscheinen transparent als `Unclassified`, sodass die Domaenenverteilung mit der Gesamtzahl reconciliert. Passed, Not configured, Duplicates und Conflicts stehen je Domain sowie global als separate, anklickbare Kontextzahlen bereit und fliessen nicht in Findings, Critical oder High ein. 38 fokussierte Dashboard-/GPO-Regressionen, Client-Typpruefung, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Im Gesamtlauf bestanden 1.070 Tests, acht optionale PostgreSQL-Tests wurden uebersprungen; zwei gleichzeitig bestehende Maester-Result-Message-Tests scheitern ausserhalb dieser Aenderung. Ausschliesslich synthetische Inhaltspruefung, keine Daten-, API-, Persistenz- oder Schemaaenderung. Phase 7 bleibt abgeschlossen; Risiko einer erzwungenen falschen Domainzuordnung wird durch den sichtbaren Unclassified-Rest vermieden.
|
||||
|
||||
Work-Standardeinstieg 2026-09-09 (Phase 7, Nutzerpraezisierung): Jeder explizite Wechsel in den Bereich Work setzt die Ansicht jetzt auf Reports. Sidebar, Client-Info-Weiterleitung und Pathfinder-Fundsprung verwenden dafuer denselben Einstieg; gespeicherte Daten, Auswahlwerte und die anderen Work-Ansichten bleiben unveraendert. Die fokussierte Regression mit drei Tests sowie der Gesamtlauf mit 1.026 bestandenen Tests, acht uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung waren erfolgreich. Der Server-Produktionsbuild war erfolgreich. Der vollstaendige Build bleibt unabhaengig von dieser Aenderung an der bereits vorhandenen temporaeren Datei `tests/maester-audit.temporary.test.ts` blockiert (`DatasetSheet.headers` existiert im Typ nicht); der direkte Client-Bundle-Ersatzlauf war in der Sandbox wegen `spawn EPERM` nicht ausfuehrbar. Keine Daten-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen.
|
||||
|
||||
Bereichswechsel-Performance 2026-09-09 (Phase 7, Nutzerauftrag): Bremsen waren mehrfach ausgefuehrte Filter-/Facetten- und Statuspruefungen, quadratische Neulesung abgelehnter nummerierter Spaltengruppen, Normalisierung im Sortiervergleich, unsichtbare Raw-Data-/Exportaufbereitung, ein uebergrosses erstes Virtualisierungsfenster und 2.537 sofort gemountete Blattoptionen. Gemeinsame begrenzte blattgebundene Selektoren, profilgebundene Statusantworten, einmalige Gruppen-/Sortierschluessel sowie bedarfsgeladene Rohtexte, Exporte und native Optionen beseitigen diese Wiederholungen. Pathfinder startet erst bei Aktivierung, pausiert versteckte Kontext-/Geometriearbeit und verwirft alten Zustand bei Workspacewechsel; Textkollisionen laufen nur bei sichtbarem Text, MITRE-Header werden einmal klassifiziert. Unveraenderte Health-/Geometriewerte ersetzen keinen Zustand. Am ausdruecklich freigegebenen Dummy (1280 x 720) sank Work von anfangs 5.324 ms auf 121-211 ms bei unveraenderten Wiederbesuchen; nach Filterreset/Graphneubau ein Rueckwechsel 406 ms. Synthetisch mit 8.257 Records: wiederholte Ableitung 398 -> rund 1 ms, null erneute Quellfeldzugriffe. 1.024 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. App, Backend und Proxy HTTP 200; Messcode fehlt im Produktionsbundle. AGENTS.md und docs/architecture/navigation-performance.md enthalten Ursachen, Messverfahren und Regressionen. Keine Migration, kein Reimport oder gezielte Datenmutation; Prueftab geschlossen. Phase 7 bleibt abgeschlossen, andere Phasenstatus unveraendert.
|
||||
|
||||
Client-Info Verbindungskurven 2026-09-08 (Phase 7, Nutzermeldung): Die Kurven zwischen Overall Score und Security Domains endeten sichtbar neben ihren Knoten. Ursache waren feste Zielkoordinaten in einem ungleichmaessig gestreckten Koordinatensystem (`viewBox="0 0 1000 640"` mit `preserveAspectRatio="none"`): sie trafen nur bei genau einer Fenstergroesse. Die Geometrie entsteht jetzt aus der tatsaechlichen Lage von Ring und Domainknoten, das Koordinatensystem entspricht der gemessenen Flaeche, und neu gemessen wird sowohl bei Groessenaenderungen der beteiligten Flaechen als auch beim Fensterereignis. Gemessen am freigegebenen Dummy bei 1.000, 820 und 805 Pixel Breite: Abweichung zwischen Kurvenende und Knotenmitte jeweils 0 Pixel, zuvor bis zu 80 Pixel. Unterhalb des Umbruchs bleiben die Kurven wie bisher ausgeblendet. Keine Aenderung an Werten, Farben oder Kachelgeometrie. 1.011 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich.
|
||||
|
||||
GPO-Quellenanzeige 2026-09-08 (Phase 7/13B, Nutzerkorrektur): Work verwendet die vom bestehenden Backend-Readmodell ausgewiesenen aktuellen Evaluationen und gruppiert Laeufe nach Aggregation statt nach der ergebnisabhaengigen Teilmenge referenzierter Backups. Reprocessing-Historie und abgeloeste Profil-/Snapshotkontexte erscheinen damit nicht als zusaetzliche Scans. Vergleichs-, Not-configured- und Konsistenzzeilen tragen den urspruenglichen Importzeitpunkt; der Auswertungszeitpunkt bleibt separat als `Evaluated` erhalten. `Source: Scan` steht auch auf einem reinen GPO-Blatt bereit. 48 fokussierte Tests und der Gesamtlauf mit 1.014 bestandenen Tests, acht uebersprungenen optionalen PostgreSQL-Tests, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Read-only am ausdruecklich freigegebenen Dummy ueber den korrigierten Workspace-Lesepfad bestaetigt: genau eine GPO-Quellenoption mit 3.805 Zeilen, ein urspruenglicher Importzeitpunkt, ein aktueller Auswertungszeitpunkt und insgesamt fuenf Quellenoptionen; die elf historischen Evaluationen sind vor und nach der Pruefung unveraendert vorhanden. Liveausgabe ausschliesslich technische Zaehler, App und Backend HTTP 200. Keine Migration, Datenmutation, Neuanalyse oder Backendneustart; Phasenstatus unveraendert.
|
||||
|
||||
Client-Info Antwortzeit 2026-09-08 (Phase 7, Nutzermeldung): Die Ansicht blockierte beim Aufbau messbar mehrere Sekunden. Ursache war nicht die Facettenableitung (5 ms), sondern die Ableitung je Datensatz: Ergebniszustand, Schweregrad, Domainzuordnung, Quellenzuordnung und vierzehn Punkteberechnungen liefen fuer alle Datensaetze des Mandantenblattes - im Dummy 8.257 - bei jedem Rendern erneut, also auch bei jeder Zustandsaenderung der Anwendung. Gemessen 3.123 ms je Aufbau. Die Ableitung entsteht jetzt einmal je Blatt: ein Durchlauf statt vierzehn, die Domainzuordnung baut den Klassifikationstext einmal statt achtmal, Zustand, Schweregrad und Klassifikationstext haengen als schwache Verweise am Datensatz, und Facetten samt Auswertung ueberdauern den Ansichtswechsel. Zusaetzlich bleiben die Umgebungsdetails und die Ersatzzaehlung der GPOs memoisiert, und die workspaceweiten Herkunftswerte werden je Datensatzliste einmal gezaehlt. Gemessen nach der Aenderung: Ableitung 0 ms je Aufbau, laengste blockierende Aufgabe beim Ansichtswechsel unter 100 ms statt ueber 4.000 ms. Fachliche Werte, Zaehlungen und Darstellung sind unveraendert; die geteilte Statuslogik der Profile wurde bewusst nicht angetastet, damit Client Info und Work weiterhin dieselben Zahlen zeigen. Der bekannt hohe Aufbau der Work-Ansicht bleibt davon unberuehrt und ist nicht Teil dieser Aenderung. 1.007 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich.
|
||||
|
||||
Abschluss Dummy-Reimport 2026-09-08 (Phase 7/13C): Der zuvor offene Punkt des vorherigen Eintrags ist erledigt. Nach dem Neustart des lokalen Backends wurde der Purple-Knight-Scan ueber den Importdialog erneut ersetzt (`run-batch-1788870808672-141`); die bestaetigungspflichtige Indicators-Datei traegt jetzt Profilversion 1.2.0 bei unveraenderter Mappingversion 1.1.0, die Zusammenfassungsdatei wird ohne Rueckfrage mit hoher Sicherheit als Purple-Knight-Quelle erkannt. Alle aktiven Scans des Dummy fuehren damit den aktuellen Verarbeitungsstand: Purple Knight 1.2.0 (20 generische Begleitdateien unveraendert 1.0.0), CIS-CAT 1.4.0, PingCastle 1.4.0, Maester 1.0.0. Datahub meldet fuer alle fuenf Eintraege `Ready`, die Reimportempfehlung ist verschwunden. Befundzahlen und Umgebungskontext unveraendert gegenueber der vorherigen Pruefung (119 Vorkommen, 22 Beobachtungen im Purple-Knight-Lauf), Client Info zeigt weiterhin 49 Faktenzeilen ohne unbekannte Werte. Keine Code-, Schema- oder Persistenzaenderung.
|
||||
|
||||
Dummy-Reimport und CIS-CAT-Titelseite 2026-09-08 (Phase 7/13C, ausdrueckliche Nutzerfreigabe): Der freigegebene Dummy wurde ueber den regulaeren Reimportweg (`/import-batches/preview` und `/import-batches/complete` mit `replacesScanRunId`) mit exakt denselben Originaldateien neu importiert; die SHA-256-Identitaet aller 26 Dateien wurde vorab und serverseitig geprueft. Vor dem Schreiben entstand ein Custom-Format-Backup, dessen Archivinhalt (345 Eintraege) und Kopie per SHA-256 verifiziert wurden. Befundzahlen bleiben unveraendert (Purple Knight 119, Maester 318, CIS-CAT 411, PingCastle 37), abgeloeste Laeufe bleiben als Historie erhalten. Der bereits am 2026-08-25 abgeloeste PingCastle-Lauf wurde bewusst nicht angefasst. Die zuvor benutzerbestaetigte Werkzeugentscheidung der Indicators-Datei wurde unveraendert reproduziert. Am echten Bericht zeigte sich, dass CIS-CAT Pro System- und Benchmarkangaben nicht als beschriftete Wertepaare fuehrt, sondern auf der Titelseite: bewertetes System als `for <Host>`, Netzadresse als `Target IP Address`, Benchmark samt Version als Ueberschrift und Profil, Zeitpunkt sowie Dauer als unbeschriftete Listenpunkte. Die Auswertung liest jetzt vorrangig diesen Block, faellt auf beschriftete Wertepaare zurueck und leitet aus einer reinen XCCDF-Ergebniskennung im Titel einen lesbaren Benchmarknamen ab; die synthetische Fixture bildet diesen Aufbau nach. Ein Betriebssystemname entsteht weiterhin nicht, weil der Bericht keinen nennt. Purple-Knight-Profil 1.2.0 und CIS-CAT-Profil 1.4.0 sind als `compatible` mit `environment-context` und Reimportbedarf im Verarbeitungsmanifest registriert; Kompatibilitaetsschluessel, Feldabbildung und Normalisierung bleiben unveraendert. Client Info zeigt fuer den Dummy jetzt 49 Faktenzeilen ohne einen einzigen Gedankenstrich. 1.004 Tests bestanden, acht optionale PostgreSQL-Integrationstests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Offen: Der lokale Backendprozess laeuft noch mit dem Stand vor dieser Aenderung, deshalb traegt die bestaetigungspflichtige Indicators-Datei weiterhin Profilversion 1.1.0 und der Purple-Knight-Scan bleibt als `Reimport recommended` markiert; nach einem Neustart des Backends genuegt ein erneuter Reimport dieses einen Scans.
|
||||
|
||||
Werkzeugkontext ausserhalb der Findings 2026-09-08 (Phase 7/13C, ausdruecklicher Nutzerentscheid Variante b): Purple Knight und CIS-CAT liefern jetzt Umgebungskontext statt nur Befunde. Ein eigener Purple-Knight-Zusammenfassungsextractor liest die zweispaltige Kennzahlendatei als reine Umgebungsbeobachtungen ohne Findingkandidaten: Posture Score, sechs Kategoriewerte, Auswertungsbilanz (evaluated, IOEs found, passed, not selected, not relevant), Tooledition, Scandauer und Anforderer. Werkzeugneutrale Angaben werden auf die gemeinsamen Eigenschaften `forest.name`, `domain.count` und `report.generated_at` abgebildet, alles uebrige bleibt unter `purple_knight.*`. Zwei zusaetzliche Inhaltssignale fuehren die Datei als Purple-Knight-Quelle statt als generische Tabelle. Der CIS-CAT-Extractor 1.3.0 ergaenzt Systemangaben aus beschrifteten Wertepaaren (Host, Betriebssystem, IP, Profil, Zeitpunkt), Benchmarkname, Gesamtscore, die deklarierten Ergebniszahlen je Zustand sowie den Punktwert je oberster Benchmarkgliederung; die vollstaendige Abschnittsuebersicht bleibt als Berichtsabschnitt erhalten. Fehlt eine Beschriftung, entsteht keine Beobachtung statt eines geratenen Wertes. Client Info zeigt die neuen Werte in der zusaetzlichen Zone `Environment Detail` mit Verzeichnis, Bestand, Passwortrichtlinie, Zertifikaten/PKI, Wiederanlauf und beschrifteten Werkzeugkennzahlen; unbekannte Fakten bleiben `—`, lizenzbeschraenkte Abschnitte werden ausdruecklich als `Restricted` benannt und nie als Null gezaehlt. Die Security-Domain-Knoten tragen jetzt den vergleichbaren ASSESSOR-Domainscore, jede Quellkachel nennt zuerst diesen Score und die native Toolzahl nur beschriftet daneben. Die GPO-Anzahl stammt aus der Verarbeitungsliste der Sicherungen statt aus einer GUID-Schaetzung in Findingtexten. Keine Schema-, Migrations- oder Persistenzaenderung; bestehende Importe tragen die neuen Beobachtungen erst nach einem erneuten Import derselben Dateien. 1.003 Tests bestanden, acht optionale PostgreSQL-Integrationstests uebersprungen; Local-only-/Designpruefung mit 328 Tokens und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung; die Datenlage wurde am ausdruecklich freigegebenen Dummy nur strukturell erhoben. Offen bleibt die Textbereinigung der PingCastle-Zellattribute in der Quellextraktion, heute nur praesentativ abgefangen. Phase 7/13C bleiben in ihrem bisherigen Status.
|
||||
|
||||
Security-Template-Parserkorrektur 2026-09-08 (Phase 13B): Parser 1.1.0 ueberspringt Metadaten und nicht unterstuetzte INF-Sektionen vor der Key/Value-Pruefung. CSV-artige Service-Eintraege lassen gueltige System-Access-, Kerberos-, Privilege-Rights- und Registry-Values-Instruktionen erhalten. Einzelne defekte unterstuetzte Eintraege degradieren das Dokument mit Diagnose auf `partial`; unsichere Abschnittsgrenzen und Ressourcenverletzungen bleiben blockiert. Baseline-Analyzer 1.2.0 und GPO-Processing/Analyzer 2.2.3 trennen Neuanalysen von unveraenderter Historie; alte Baseline-Analyserversionen bleiben lesbar. Analysegebundene ADMX-Katalog-IDs beheben den bei der Bestandsneuanalyse belegten PostgreSQL-Konflikt fuer unveraenderte Kataloginhalte. 990 Tests bestanden, acht optionale PostgreSQL-Integrationstests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Custom-Format-Backup vor Live-Neuanalyse vollstaendig auf Archivlesbarkeit sowie die lokale Kopie per SHA-256 geprueft; kein Restore behauptet. Alle elf Baselinepakete neu analysiert: 5.031 statt 4.419 Einstellungen, darunter 1.039 statt 427 Security-Template- und 400 statt 146 Privilege-Rights-Eintraege. Alle elf alten Analysen bleiben erhalten, aktiv sind nur die elf Nachfolger. Dummy neu ausgewertet: konfigurierte Kundenwerte ergeben 113 Matches, 98 Abweichungen und 15 Reviews; 28 zuvor fehlende Privilege-Rights-Abweichungen kommen hinzu, 13 bisherige Abweichungen werden aufgrund der wiederhergestellten Baselinevarianten zu Review, zwei alte Reviews bleiben bestehen. Alle 15 Reviews enthalten den Baseline-Consensus-Konfliktcode. Zusaetzlich bestehen 3.575 Vergleiche mit fehlendem Kundenwert sowie 71 entsprechende Review-Faelle; diese sind keine konfigurierten Abweichungen. Die Erwartung 107 Abweichungen plus 2 Review wurde dadurch korrigiert. Unabhaengiger Abgleich gegen den Excel-Policy-Analyzer-Export des Dummy-Datensatzes bestaetigt die Korrektur: alle 109 dort sichtbaren Abweichungen sind je Baseline vollstaendig abgedeckt (Abweichung plus Review), zuvor waren es 83; Matches stimmen je Baseline ebenfalls ueberein. Vier zusaetzliche ASSESSOR-Abweichungen betreffen `SeEnableDelegationPrivilege` in 22H2, 23H2, 24H2 und 25H2, wo die Baseline das Recht ausdruecklich leer setzt; Policy Analyzer stellt diese explizit leere Empfehlung nicht von fehlender Abdeckung unterscheidbar dar. Der Vergleich erfolgte ausschliesslich auf dem Dummy-Datensatz. Betriebsnebeneffekt der automatischen Job-Reparatur siehe Risikoabschnitt; App, Backend und Proxy-Health HTTP 200. Phase 13B bleibt in Arbeit.
|
||||
|
||||
GPO-Klassifikation in der Result DB 2026-09-07 (Phase 7/13B, ausdruecklicher Nutzerwunsch): Die Result DB behaelt Outcome als werkzeuguebergreifende Achse und ergaenzt die unabhaengige Facette `GPO Classification` mit Misconfigured, Not configured, Match, Conflict, Duplicate und Review required. Deterministische Kundenkonflikte und Dubletten werden aus den aktuellen Policy-Beobachtungen ueber dieselbe Gruppierungslogik wie Work als eigene reine Leseprojektionen erzeugt; beide bleiben `warning`, `Deviation: No` und veraendern weder Originale noch Evaluationen. Die Auswahl einer GPO-Klasse loest den standardmaessigen Deviation-Filter, damit nicht abweichungsartige Klassen erreichbar bleiben. Lokales und PostgreSQL-Repository verwenden denselben Domainvertrag; API-Query und Detailansicht fuehren die Klassifikation und bei Konsistenzgruppen die Quellenanzahl. Keine Migration, kein Reimport und keine Datenmutation. 976 Tests bestanden, 8 optionale PostgreSQL-Integrationstests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung; Phasenstatus unveraendert.
|
||||
|
||||
Client-Info Domain-Findinganzahl 2026-09-07 (Phase 7, Nutzerpraezisierung): Security Domains zeigt als primaere Zahl ausschliesslich die Anzahl der Deviations mit der Beschriftung Findings. Passzahlen, prozentuale Domainscores und Prozentbalken entfallen aus den Domainzeilen; der bestehende Deviation-Drilldown bleibt erhalten. Die neutralen Domain-Icons stellen keinen aus der Anzahl abgeleiteten Schweregrad dar.
|
||||
|
||||
Client-Info Deviation-Drilldown 2026-09-07 (Phase 7, Nutzerpraezisierung): Fachliche Spruenge aus Client Info nach Work aktivieren standardmaessig `Deviation`, einschliesslich Security Domains, Prioritaeten und Quellen. Die bisherige ausdrueckliche All-Auswahl bei Domain-/Finding-Klicks entfaellt. Assessment Gaps behaelt seine separate Statusauswahl. Domain-Scoreberechnung bleibt unveraendert; der Work-Drilldown zeigt nur Abweichungen innerhalb der zugeordneten Record-IDs. Synthetische Regression deckt Failed, Passed und Skipped innerhalb einer Domain plus fachfremdes Failed ab.
|
||||
|
||||
Verifikation Domain-Drilldown 2026-09-07: 974 Tests bestanden, acht optionale PostgreSQL-Integrationstests uebersprungen; Client-/Serverbuild sowie Local-only- und Designpruefung erfolgreich. Ausschliesslich synthetische Inhaltspruefung; keine erneute Live-Dateninspektion.
|
||||
|
||||
Client-Info Domain-Drilldown 2026-09-07 (Phase 7): Die praesentative Domainzuordnung beruecksichtigt Titel und fachliche Beschreibungs-/Kategoriewerte statt saemtlicher Feldnamen und Evidence-Inhalte. Gemeinsame Aggregatspalten wie User/Baseline fuehrten zuvor dazu, dass einzelne Domains praktisch alle Records enthielten. Work erhaelt weiterhin die exakten Record-IDs der angeklickten Domain; der Wechsel deaktiviert eine vorherige My-Workspace-Einschraenkung. Fuenf synthetische Regressionen pruefen vier getrennte Domains durch den produktiven Work-Filter sowie fehlende Domainzuordnung; 17 fokussierte Tests erfolgreich. Domain-Scores koennen durch die korrigierte Zuordnung abweichen; Overall-Score und Persistenz bleiben unveraendert.
|
||||
|
||||
Client-Info Work-Kachelfarbe 2026-09-07 (Phase 7, ausdrueckliche Nutzerpraezisierung): Alle Client-Info-Kacheln beziehen Oberflaeche, Hover, Text und Trennlinien jetzt aus denselben Theme-Rollen wie die Work-Kacheln (`--tile-surface`, `--tile-surface-hover` und zugehoerige semantische Rollen). Die Command-Center-Komposition und ihre fachlichen Akzentfarben bleiben gekapselt erhalten; damit bleibt der Kontrast auch bei einem Themewechsel valide.
|
||||
|
||||
Client-Info Standardhintergrund 2026-09-07 (Phase 7, ausdrueckliche Nutzerpraezisierung): Die aeussere Client-Info-Seite verwendet wieder den gemeinsamen Theme-Hintergrund `--surface-app`. Struktur, Inhalte, Bewertung und Navigation bleiben unveraendert. Die gekapselte Client-Info-Designregel wurde entsprechend praezisiert und durch die folgende Work-Kachelfarbentscheidung weiter konkretisiert.
|
||||
|
||||
Client-Info Security Command Center 2026-09-07 (Phase 7/13C, ausdruecklicher Nutzerentscheid): Die geladene Client Info verwendet jetzt die verbindliche dunkle Referenzkomposition statt der bisherigen allgemeinen Kacheloptik. Die Ansicht zeigt eine kompakte Icon-Faktenzeile fuer AD-Domains, Entra-Tenants, Benutzer, Computer, Domain Controller und eindeutig aus Provenienz ermittelte GPOs; darunter stehen Overall Score und acht per SVG-Kurven verbundene Security Domains, eine separate Priority-/Gap-Aktionsspalte und die werkzeugspezifische Source Overview fuer PingCastle, Purple Knight, Maester, CIS-CAT und GPO Analysis. Alle Werte entstehen aus dem aktiven Timeslot, normalisierten Findings und der vorhandenen Environment-Snapshot-Grenze; unbekannte Fakten bleiben `—`. Der Overall Score bleibt der gleichgewichtete Mittelwert verfuegbarer ASSESSOR-Quellenscores, Coverage und Critical Findings bleiben getrennte Guardrails. Native Toolmetriken werden nur eindeutig beschriftet in der Source Overview gezeigt. Die neue `.client-command-center`-Regel ist die einzige Ausnahme von der allgemeinen Kachelkomposition und kann nicht in andere Ansichten auslaufen. Keine Parser-, Import-, API-, Persistenz- oder Schemaaenderung. 969 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung mit 328 Tokens und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Live-Sichtpruefung ausschliesslich am ausdruecklich freigegebenen Dummy bestaetigte Fakten, Ring, Verbindungskurven, Domains, Prioritaeten, Gaps und Source-Metriken. Phase 7/13C bleiben in ihrem bisherigen Status.
|
||||
|
||||
Workspaceweite Source-Scan-Navigation 2026-09-07 (Phase 7, ausdrueckliche Nutzerpraezisierung): `Source: Scan` wird in jeder Fachansicht aus dem gemeinsamen projektweiten All-Records-Blatt aufgebaut und bietet deshalb auch aus der reinen GPO-Ansicht jederzeit alle geoeffneten Scannerlaeufe an. Die Optionen werden weder durch das aktive Blatt noch durch andere Filter reduziert. Die Auswahl eines Scans aus einem dedizierten Fachblatt wechselt auf das gemeinsame Aggregat und setzt dort im selben Bedienvorgang den stabilen Provenienzfilter; `All` bleibt die ungefilterte Auswahl. Der Facettenvertrag verwendet dafuer unabhaengig von der Quellspaltenposition die feste ID `provenance:source-scan`. Daten, Scannerzuordnung und Persistenz bleiben unveraendert. 33 fokussierte Filter-/GPO-Regressionen gruen; 971 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung sowie Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Filteranordnung 2026-09-07 (Phase 7, ausdruecklicher Nutzerwunsch): Die reine GPO-Ansicht zeigt ihre fachlichen Filter ohne kuenstlichen Zwischenraum von links nach rechts. Der Provenienzfilter `Source: Scan` ist in jeder Ansicht dauerhaft vorhanden und wird unabhaengig von Einwertigkeit oder GPO-Facettenlimit als letzter Filter ganz rechts angeordnet; fehlt eine gleichnamige Quellspalte, entsteht ein rein praesentativer Fallback aus der vorhandenen Scan-/Profilbezeichnung. Die GPO-Ansicht darf dafuer elf sichtbare Facetten fuehren, sodass Status, die neun GPO-Fachfacetten und `Source: Scan` gleichzeitig sichtbar bleiben. Filterwerte und Datensatzprojektion bleiben unveraendert. 32 fokussierte Filter-/GPO-Regressionen gruen; 970 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung sowie Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion; Phasenstatus unveraendert.
|
||||
|
||||
Kundenreduzierte GPO-Quellenanzeige 2026-09-07 (Phase 7/13B, ausdruecklicher Nutzerwunsch): Sichtbare GPO-Sources bestehen nur noch aus der GPO-internen Referenz ab `{GPO-GUID}/DomainSysvol/GPO/...` beziehungsweise ersatzweise ab dem eindeutig erkannten `DomainSysvol/GPO/...`-Segment. Lokale Laufwerks-, Ordner-, Backup- und Artefaktpraefixe werden niemals gerendert; ohne sicher erkennbaren internen Anker erscheint fail-closed `GPO source reference unavailable`. Normale GPO-Sources sowie Conflict-/Duplicate-Evidence verwenden denselben reinen Presenter. In Results stehen die Referenzen sofort unter Evidence als einzelne technische Pfadzeilen, ohne `Show source` und ohne wiederholte fette Backup-/GUID-Ueberschrift. Vollstaendige Provenienz bleibt unveraendert gespeichert; Backup-/Record-IDs bleiben nur in der bestehenden eingeklappten technischen Detailstufe. Vier Source-, elf Conflict- und neun Duplicate-Regressionen sowie 36 fokussierte Tests gruen; 967 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Duplicate-Lesepipeline 2026-09-07 (Phase 7/13B): Duplicate-Results verwenden jetzt dieselbe erklaerende Reihenfolge wie Conflicts: `Duplicate detected` (warum) -> `Setting details` (was betroffen ist) -> `Observed value` (identischer Wert/Typ und Evidence) -> eingeklappte technische Quelldetails. Der Hinweis stellt klar, dass ein Duplicate allein keine Baselineabweichung ist und beabsichtigt sein kann. Der gemeinsame Wert erscheint in einer responsiven Tabelle; alle Quellen sind direkt am Evidence-Feld aufklappbar, vollstaendige Pfade und interne IDs bleiben in der letzten Detailstufe. Exakte Leer-/Whitespacewerte, 2.300-Quellen-Skalierung, sichere Envelopevalidierung und Tabellenfallback bleiben erhalten. Duplicate- und Conflictprojektionen teilen die neutrale strukturierte Darstellung, Bewertung/Persistenz/Originaldaten bleiben unveraendert. Neun Duplicate- und elf Conflictregressionen, 36 fokussierte Resulttests gruen; 967 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Conflict-Lesepipeline 2026-09-07 (Phase 7/13B, ausdrueckliche Nutzerkorrektur): Die Conflicts-Ansicht folgt jetzt `Conflict detected` (warum) -> `Setting details` (was betroffen ist) -> `Observed variants` (was beobachtet wurde) -> eingeklappte technische Quelldetails. Die unmittelbar zuvor eingefuehrten einzelnen Variantenkarten sind durch eine gemeinsame Tabelle mit Variant, Value, Type und Evidence ersetzt. Quellen bleiben je Variante direkt aufklappbar; lange Werte umbrechen vollstaendig und nur die Tabelle erhaelt an schmalen Viewports eine eigene horizontale Rollflaeche. Keine Gewinner-/Praezedenzbehauptung, Normalisierung oder Datenmutation. Elf Konfliktregressionen und 34 fokussierte Resulttests gruen; 965 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Conflict-Results-Erklaeransicht 2026-09-07 (Phase 7/13B): Die bisherige doppelte generische Tabellenansicht fuer GPO-Konflikte ist durch eine strukturierte, neutrale Variantenansicht ersetzt. Ein Warnhinweis erklaert Konflikt und unbekannte Wirksamkeit, Variantenkarten stellen exakte Werte, lesbare Registrytypen und zugeordnete Quellen gegenueber; leere und reine Whitespace-Werte werden ausdruecklich bezeichnet. Settingkontext folgt separat, interne Backup-/Record-IDs bleiben vollstaendig unter eingeklappten technischen Details erhalten. Die Darstellung entsteht erst nach vollstaendiger Validierung des begrenzten Evidence-Envelopes; ungueltige/alte Projektionen fallen auf die bestehende inerte Tabellenansicht zurueck. Duplicate-Results, Vergleich, Persistenz und Originaldaten bleiben unveraendert. Zehn Konfliktregressionen und 33 fokussierte Resulttests gruen; 964 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Ausschliesslich synthetische Inhaltspruefung, keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Kachelbadges 2026-09-06 (Phase 7): TileCard verwendet jetzt dieselbe GPO-Kategorieableitung wie der FindingReader und ergaenzt Scope. Gilt auch fuer gemischte Workspaces; doppelte Kategorien/Scopes werden vermieden, andere Scanner bleiben unveraendert. Bestehende Badge-Matrix und unbegrenzte normale Kacheldarstellung werden weiterverwendet. Vier neue synthetische Regressionen; 962 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Keine Daten-/Bewertungsaenderung, kein Reimport, keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
||||
GPO-Duplicate-Results 2026-09-06 (Phase 7): Duplicates nutzen analog zu Conflicts den bestehenden Results-Button und dieselbe skalierbare Tabellenansicht. Eine Tabelle zeigt den gemeinsamen Originalwert, Typ und Quellenanzahl; eine zweite jede Quelle mit Source path, Backup und Record. Einstellung, Scope, Registry-Pfad und Value name stehen im Kontext. Keine Konflikt-/Gewinnermarkierung bei gleichen Werten. Der Reader zeigt eine kurze Duplicate-Zusammenfassung statt wiederholter Werte/Sources. Gemeinsame begrenzte, inerte Leseprojektion mit eindeutig getrennten Envelopes und Whitespace-Erhaltung; bei unvollstaendiger/mehrdeutiger Evidenz bleibt die bisherige Darstellung erhalten. Bewertung, Persistenz und Originaldaten unveraendert; kein Reimport erforderlich, bestehende Workspaces neu oeffnen. Sieben neue synthetische Regressionen inklusive 2.300 Quellen, Buttonaktivierung, Escaping, Fallback und unveraenderten Konflikten. 958 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich (bestehender Bundle-Hinweis). Keine Realdateninspektion oder visuelle Live-Abnahme; Phasenstatus unveraendert.
|
||||
|
|
@ -1316,6 +1392,10 @@ Abnahmekriterien:
|
|||
|
||||
Status: in Arbeit
|
||||
|
||||
Zwischenstand 2026-09-09 (PurpleKnight): Workbook-Findings neben Summary wiederhergestellt; HTML-Berichtskontext, Originalwerte/Provenienz, Null-Details und Vollstaendigkeitsdiagnosen verifiziert. Profil 1.3.0 empfiehlt Reimport bei unveraenderter Historie. Dummy-Abgleich und 25 synthetische Regressionen sind abgeschlossen; damit ist keine pauschale Abnahme aller Scanner-Versionen oder der gesamten Phase 13C verbunden.
|
||||
|
||||
Zwischenstand 2026-09-09 (PingCastle-Auditpunkte 1-3): Strukturtreue der Attribute, Leerzustaende und HTML-Zellwerte mit synthetischen Browser-/Serverregressionen, unabhaengiger Dummy-Strukturzaehlung und produktivem Parse-only-HTTP-Abgleich verifiziert. Profil 1.5.0 empfiehlt Reimport, aendert aber keine Historie. XML bleibt vorerst generisch unter `pingcastle.xml.*`; gemeinsame semantische Schluessel und HTML-Control-Path-JSON sind weiterhin offen. Eine gleiche Findingzahl allein gilt nicht als Abnahme des gesamten Berichtskontexts.
|
||||
|
||||
Zwischenstand 2026-09-06 (Baseline-Vollreimport): Der bestaetigte Delete-Pfad entfernt nun auch ausschliesslich reproduzierbare Policy-Evaluations- und Finding-Ketten, die die geloeschte Baselineanalyse referenzieren. Kundenimporte, GPO-Beobachtungen, Environment Profiles und Snapshotaggregationen bleiben erhalten. Damit ersetzt diese Entscheidung die fruehere Delete-Sperre fuer bereits verwendete Baselines. Vor der produktiven Loeschung wurde der kanonische PostgreSQL-Bestand als Custom-Format gesichert und der Dumpkatalog gelesen. Der letzte blockierte Baselineeintrag wurde geloescht; der Katalog ist leer und 51 GPO-Beobachtungen sind weiterhin vorhanden. Der Neuimport selbst erfolgt anschliessend durch den Benutzer.
|
||||
|
||||
Ziele:
|
||||
|
|
@ -1549,6 +1629,26 @@ Aktualisierung nach der Katalog-Ladegrenze vom 2026-09-03: Fachlich freigegebene
|
|||
|
||||
## 9. Bereits getroffene Architekturentscheidungen
|
||||
|
||||
Festlegung 2026-09-10 (Purple-Knight-Begleitdateien): Ein nativer Score darf aus einer separaten Summary auf die Findings desselben Scanlaufs uebertragen werden, wenn genau ein nativer Traeger und eine eindeutige Ergebnispopulation denselben Werkzeug- und Verarbeitungskontrakt teilen. Datei- und Vorkommensherkunft bleiben gemeinsam erhalten; Mehrdeutigkeit verhindert die Verbindung.
|
||||
|
||||
Festlegung 2026-09-10 (weitere Scanner): Native Gesamtwerte werden bei jeder Profilerweiterung zuerst geprueft und deklarativ mit ihrer Bedeutung erfasst. Die Scoredefinition ist Pflicht, die Berechnung gemeinsam; ihre Version gehoert zur Vergleichskompatibilitaet. Extraktion und Originalherkunft bleiben im generischen Importpfad. AGENTS.md und docs/SCORING_MODEL.md halten die Erweiterungsregel fest.
|
||||
|
||||
Festlegung 2026-09-10 (Phase 7/13C/8, Nutzerfreigabe): Die bisher nur sekundaere native Scoreanzeige wird durch das versionierte hybride Beitragsmodell in `docs/SCORING_MODEL.md` ersetzt. Unterschiedliche native Risikomodelle und Bestehensquoten bleiben ausdruecklich methodisch verschieden; keine Severitygewichte oder Domainzuordnungen werden erfunden. Scoring bleibt plattformneutrale Fachlogik ausserhalb von React. Originale und gespeicherte historische Assessments bleiben unveraendert.
|
||||
|
||||
Festlegung 2026-09-09 (Phase 13/13A, HTML-Timeout): Attributtoken werden monoton und ohne unanchored name=value-Backtracking gelesen. Gemeinsam verwendete Verweisdefinitionen werden nur innerhalb ihres exakten unveraenderten Markups indiziert; keine globale oder dateinamenbasierte Wiederverwendung. Mehrdeutigkeit, erste Definition, Zielreihenfolge, Vollstaendigkeit und Originalwerte bleiben Vertragsbestandteile. Leistung wird ueber Zeichenarbeit und Inhaltsparitaet abgesichert; der gebaute isolierte Worker muss mit unveraenderten Sicherheitsgrenzen bestehen. Die Fehleranzeige allein ist kein Beleg fuer die konkrete Struktur der realen Quelldatei.
|
||||
|
||||
Festlegung 2026-09-09 (Phase 13A/13C, PurpleKnight): Kontext-Extractors duerfen keine anderen Findingtabellen desselben Dokuments verdraengen; sie komponieren deren generische Extraktion unter Erhaltung der Quellindizes. Native Berichtssyntax gehoert in den registrierten Source Extractor, der gemeinsame statische Leser bleibt werkzeug- und plattformneutral. HTML-Metriken verwenden dieselben semantischen Kontextvertraege wie die Zusammenfassungstabellen; unbekannte Originaldaten bleiben separat erhalten. Abgeleitete Anzeigefelder zeigen auf reale Eingabeobjekte statt erfundene Quellfelder. Zeitpraezision wird nicht fuer Formatparitaet reduziert. Fehlende IDs und unlesbare/konkurrierende Objekte scheitern, doppelte Regel-IDs bleiben getrennte Vorkommen mit Diagnose. Profil 1.3.0 versioniert Erfassung und Evidenz; Mapping/Normalisierung/Praesentation 1.1.0 und kanonischer Kompatibilitaetsschluessel bleiben erhalten. Reimport ist empfohlen, aber ohne ausdruecklichen Auftrag nicht durchgefuehrt.
|
||||
|
||||
Festlegung 2026-09-09 (Phase 13A/13C, PingCastle): Originale bleiben unveraendert. XML-Attribute werden als exakte Name/Wert-Paare mit eigenen Quellpfaden neben Text und expliziter Leere gefuehrt; Attribut-Anzeigezeichenketten sind kein verlustfreies Austauschformat. Der generische HTML-Leser schuetzt zitierte Attributgrenzen, bereinigt nur ganze Attribute und behandelt abgeschnittene Textausschnitte getrennt von vollstaendigen Dokumenten. Native Rule IDs sind erforderlich. Die historische First-Presentation-Deduplizierung bleibt erhalten, wird aber mit einer Warnung sichtbar. Kategorie-Trefferzahlen werden gegen eindeutige Regeln, nicht gegen Kataloggroessen geprueft; Originalzaehler erhalten einen separaten Reportabschnitt ausserhalb der Assetbeobachtungen. Profil 1.5.0 und Extractor 2.2.0 versionieren die Aenderung; Mapping 1.2.0, Normalisierung 1.1.0 und Kompatibilitaetsschluessel bleiben erhalten.
|
||||
|
||||
Festlegung 2026-09-09 (Phase 13A/13C, Maester): Eingebettete Berichtsdaten sind Daten, kein auszufuehrendes Programm. Der native Extractor erweitert die generische Pipeline und liefert codekontrollierte Werkzeugerkennung, vollstaendige Originalfelder mit Quellpfaden sowie getrennten Berichtskontext. Statische Objektliterale sind nur innerhalb der dokumentierten Grammatik und Ressourcengrenzen zulaessig; dynamische oder mehrdeutige Berichtswurzeln werden explizit abgelehnt. Semantisch mehrdeutige Feldnamen werden anhand der Spalten desselben Kandidaten beziehungsweise derselben Tabelle abgebildet, nicht anhand des Dateityps allein. Gruppierungs- und Wiederverwendungsschluessel beruecksichtigen diesen Kontext. Result-Nachricht und Statusfilter verwenden dieselbe Prioritaet; Evidenztext darf keinen zweiten Ergebnisstatus erzeugen. Native Summenzaehler werden nicht als Vollstaendigkeitsbeweis benutzt oder ueberschrieben, sondern gegen die Einzeltests abgeglichen. Maester-Profil, Mapping und Praesentation steigen auf 1.1.0 bei unveraenderter Normalisierung und Kompatibilitaetsschluessel; historische Verarbeitungsstaende bleiben lesbar und werden als Reimport empfohlen ausgewiesen.
|
||||
|
||||
Festlegung 2026-09-09 (Phase 7, Navigationskosten): Unveraenderte Filter-, Sortier- und selbst-ausnehmende Facettenprojektionen werden zwischen Verbrauchern und Bereichsbesuchen geteilt. Begrenzte schwach blattgebundene Caches fuehren alle fachlichen Eingaben im Schluessel: Profil, Dataset, Reviews, Filter, Suchtext, Sortierung und Pfadscope. Komponenten-useMemo oder wiederverwendbare IDs allein reichen nicht. Unsichtbare Rohtexte, Exportpayloads, Optionslisten und Graphbeschriftungen werden erst bei Bedarf aufgebaut. Retained Views pausieren versteckte Arbeit und uebernehmen bei Aktivierung aktuelle Eingaben; Workspacewechsel verwerfen den alten Zustand. Datenschutzarme lokale Dev-Messung bleibt ausdruecklich opt-in und ausserhalb des Produktionsbundles. Verfahren und Abnahmeregeln stehen in docs/architecture/navigation-performance.md.
|
||||
|
||||
Festlegung 2026-09-08 (Phase 7/13B, GPO-Quellen): Die bestehende Policy-Analyseuebersicht ist fuer aktuelle Profil-, Identity- und Snapshotrevisionen autoritativ. Work uebernimmt ihre aktuellen Evaluations-IDs; wechselnde Ergebnisabdeckung ist niemals eine Importidentitaet. Die zusaetzliche Auswahl je Aggregation verwendet den gemeinsamen versionierten Evaluationsselektor. Vergleiche duerfen nur erscheinen, wenn ihre referenzierten Beobachtungen vollstaendig zum geladenen Timeslot-Scope gehoeren. Import- und Auswertungszeit bleiben getrennt; fehlende Kundenwerte beziehen ihren Importzeitpunkt aus demselben Evaluationskontext. Persistierte Historie und Originale bleiben unveraendert.
|
||||
|
||||
Festlegung 2026-09-08 (Phase 13B, ausdruecklicher Nutzerwunsch): Die Security-Template-Regel vom 2026-07-21, jeden defekten unterstuetzten Eintrag zum Anlass fuer die Verwerfung des gesamten Artefakts zu nehmen, wird gezielt ersetzt. Nicht unterstuetzte Sektionen besitzen keine Key/Value-Grammatikpflicht. Isolierte fehlerhafte Eintraege werden diagnostiziert und ausgelassen, gueltige Instruktionen mit Originalwert und Quellzeile bleiben bei Dokumentstatus `partial` erhalten. Beschaedigte Abschnittszuordnung, Dekodierungs- und Ressourcengrenzen bleiben fail-closed. Die Inventarprojektion weist Teilverarbeitung aus und behauptet keine Vollstaendigkeit. Baseline-Consensus-Konflikte bleiben Review; der Fix waehlt keinen Gewinner und aendert weder Comparatoren noch Deviationfilter. Jede Baselineanalyse besitzt einen eigenen versionierten ADMX-Katalogdatensatz: Die Katalog-ID bindet deshalb Analyse-ID und Inhaltsidentitaet, auch wenn ADMX/ADML unveraendert sind. Technische Policy-Identitaeten und Kataloginhalte bleiben gleich; historische Analysen werden nicht ueberschrieben. Damit wird der beim ersten Bestandsreprocessing belegte PostgreSQL-Schluesselkonflikt behoben, ohne das Schema zu aendern.
|
||||
|
||||
Festlegung 2026-09-06 (Phase 8, fehlende Folgequellen): Timeslotvergleiche trennen kuenftig gemessenen Fortschritt, Gesamtexposition und Evidence Coverage/Freshness. Fehlende erwartete Quellen werden nicht als behoben interpretiert. Offene Verpflichtungen werden mit Herkunft und Alter als `carried-forward-awaiting-revalidation` fortgeschrieben und bleiben scorewirksam; Rohbeobachtungen werden nicht als aktuelle Evidenz kopiert. Ein Scope-Ende ist ausschliesslich ueber eine begruendete, auditierte `retired-out-of-scope`-Entscheidung moeglich. Dieser generische Vertrag gilt fuer Scanner, GPOs und spaetere Tools und wird nicht ueber ein primaeres manuelles Auswahlhaekchen gesteuert.
|
||||
|
||||
Festlegung 2026-09-06 (Phase 9/10, FindingReader-Reviews): Bearbeitbare Workflowdaten gehoeren zum kanonischen Finding-Review und damit in die ASSESSOR-Datenbank, nicht nur in Local Storage. Die stabile Occurrence-ID verbindet Aggregate und Quellansicht; Datenbankwerte gewinnen beim Laden, Local Storage bleibt Rueckfall und Migrationsquelle. Der exakte UI-Status wird additiv neben dem kanonischen Workflowstatus bewahrt; unbekannte kuenftige Marker bleiben bei Updates erhalten.
|
||||
|
|
@ -1564,6 +1664,13 @@ Festlegung 2026-09-06 (Phase 13B/13C, ausdruecklicher Nutzerwunsch): Vor Rueckfr
|
|||
Festlegung vom 2026-09-06 (Phase 11, lokaler Persistenzbetrieb): Der vorhandene Docker-Compose-Bestand `assessor-2_0` mit der Datenbank `assessor`, dem Container `assessor-2_0-postgres-1` und dem persistenten Volume `assessor-2_0_assessor-postgres-data` ist der einzige kanonische lokale ASSESSOR-Datenbestand fuer diesen Workspace. Lokale Starts verwenden ausschliesslich `127.0.0.1:5432`. Die Compose-Konfiguration bindet dieses Volume deshalb namentlich als `external`; bei fehlendem Bestand oder Portkonflikt muss der Start fehlschlagen, statt eine weitere ASSESSOR-Datenbank oder einen Ersatzport anzulegen. Diese ausdrueckliche Nutzerfestlegung ersetzt die bisherige implizite, verzeichnisabhaengige Compose-Projektidentitaet. Der interne Browser-/Backend-/PostgreSQL-Zielaufbau bleibt unveraendert.
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
| 2026-09-07 | Die geladene Client Info ist innerhalb der Wurzelklasse `.client-command-center` eine feste dunkle Command-Center-Ausnahme von der allgemeinen Kachelkomposition. Die genehmigte Hierarchie ist Client/Assessment/Source, sechs Icon-Fakten, Overall Score mit acht visuell verbundenen Security Domains, Priority Findings plus Assessment Gaps und abschliessend fuenf werkzeugspezifische Source-Flaechen. | Die ausdruecklich genehmigte Referenz ist ein zusammenhaengendes Erstgespraechs-Dashboard und keine Sammlung allgemeiner Anwendungskacheln. Eine eng begrenzte Ausnahme bildet diese Hierarchie originalgetreu ab, ohne Data Hub, Work, Result DB, Pathfinder oder Shell visuell zu veraendern. |
|
||||
| 2026-09-07 | Die Nutzerpraezisierung erweitert die unmittelbar folgende Positionsentscheidung: `Source: Scan` ist eine workspaceweite Navigationsfacette mit stabiler ID und vollstaendiger Optionsliste aus dem projektweiten All-Records-Blatt. Eine Auswahl aus einem dedizierten GPO-/Fachblatt wechselt auf dieses Aggregat und aktiviert dort direkt den ausgewaehlten Scan; lokale Facettennachzaehlung darf die Liste nicht reduzieren. | Der Filter soll nicht nur die aktuell sichtbaren Records einschraenken, sondern jederzeit den direkten Wechsel zu jedem geoeffneten Scan ermoeglichen. Nur die projektweite Quelle und ein positionsunabhaengiger Identifikator verhindern, dass ein Fachblatt alle anderen Scanner aus der Auswahl verliert oder der gesetzte Filter beim Blattwechsel unbrauchbar wird. |
|
||||
| 2026-09-07 | `Source: Scan` ist ein permanenter Provenienzfilter jeder Work-/Graph-Ansicht und steht stets als letzter Filter ganz rechts. In der reinen GPO-Ansicht ruecken die fachlichen Filter linksbuendig zusammen; deren sichtbares Limit betraegt elf statt zehn, damit Status, neun GPO-Fachfacetten und die Source-Facette gleichzeitig erhalten bleiben. Ein fehlendes explizites Source-Feld wird praesentativ aus Scan-/Profilmetadaten ergaenzt. | Die Quellenherkunft muss unabhaengig vom aktiven Werkzeug jederzeit auffindbar und an einer stabilen Position bedienbar sein. Einwertige Source-Werte sind fuer Provenienz weiterhin relevant und duerfen weder durch die allgemeine Nuetzlichkeitspruefung noch durch das Facettenlimit verschwinden. |
|
||||
| 2026-09-07 | Kunden sichtbare GPO-Quellen zeigen ausschliesslich die interne Referenz ab GPO-GUID beziehungsweise `DomainSysvol/GPO`; jeder lokale oder artefaktbezogene Praefix wird in der Praesentationsschicht entfernt. Quellen stehen unmittelbar als technische Pfadzeilen in der Evidence-Zelle, ohne aufklappbaren `Show source`-Zwischenschritt und ohne doppelte Textueberschrift. Nicht eindeutig sanitisierbare Pfade werden nicht teilweise angezeigt. | Lokale Ablage- und Backupordner sind fuer die fachliche Kundenbewertung irrelevant und koennen interne Namen offenlegen. Die GPO-GUID plus interner Policy-/Datei-Locator reicht fuer eindeutige, kundenfaehige Evidenz; fail-closed Reduktion verhindert versehentliche Preisgabe bei unbekannten Pfadformen. |
|
||||
| 2026-09-07 | GPO-Duplicates folgen derselben vierstufigen Results-Lesepipeline wie Conflicts, verwenden aber `Observed value` mit genau einer gemeinsamen Wertzeile und einen informativen Duplicate-Hinweis. Dieser Hinweis behauptet weder Fehler noch Unbedenklichkeit, sondern ausschliesslich: identische Quellenbelegung und fuer sich allein keine Baselineabweichung. | Gleiche Informationsarchitektur macht Conflict und Duplicate vorhersehbar bedienbar. Die getrennte Semantik verhindert zugleich, dass reine Quellenredundanz als Fehlkonfiguration erscheint oder eine gegebenenfalls separate Baselineauswertung verdeckt. |
|
||||
| 2026-09-07 | Die GPO-Conflict-Erklaeransicht folgt verbindlich der Reihenfolge Ursache, betroffenes Setting, beobachtete Varianten und technische Details. Varianten erscheinen in einer gemeinsamen Tabelle statt in Einzelkarten. Diese Nutzerkorrektur ersetzt fuer die Primaerdarstellung die unmittelbar folgende Kartenentscheidung; deren neutrale Semantik, Quellbindung, exakte Leer-/Whitespacewerte und sicherer Fallback bleiben erhalten. | Die Leserin soll zuerst verstehen, warum ein Konflikt vorliegt, danach den betroffenen Registrykontext erkennen und erst anschliessend die Werte zeilenweise vergleichen. Eine gemeinsame Tabelle bietet bei drei oder mehr Varianten die bessere Scan- und Vergleichbarkeit; technische IDs bleiben letzte Detailstufe. |
|
||||
| 2026-09-07 | GPO-Konflikte verwenden in Results eine erklaerende Variantenprojektion statt zweier primaerer Evidenztabellen. Jede Variante bleibt neutral, bindet ihre Quellen direkt und zeigt leere beziehungsweise reine Whitespace-Werte explizit; wirksame GPO-Praezedenz wird nicht behauptet. Vollstaendige technische Pfade und IDs bleiben eingeklappt verfuegbar, die alte Tabellenprojektion bleibt sicherer Fallback. | Die primaere Nutzerfrage ist, welche Werte sich widersprechen und durch welche Quellen sie belegt sind. Generische Tabellen wiederholten Kontext, machten leere Werte unsichtbar und zwangen zum Querlesen technischer IDs. Progressive Disclosure verbessert die Erklaerung, ohne Evidenz zu normalisieren, einen Gewinner zu erfinden oder die bestehende Vergleichs-/Persistenzgrenze zu veraendern. |
|
||||
| 2026-09-03 | Die verbindliche Client-Info-Startansicht expandiert nur `Assessment Status`; alle neun anderen Informationskarten starten minimiert. | Der Gesamtstatus bleibt ohne Interaktion sichtbar, waehrend die darunterliegende feste 3/2/2-Matrix und die rechte Rail als kompakte, stabile Navigation beginnen. Inhalte werden nicht entfernt und bleiben ueber die Standarddreiecke einzeln erreichbar. |
|
||||
| 2026-09-03 | Minimierte Client-Info-Karten bleiben ueber die volle Breite ihrer festen Spalte gespannt. `Directory Overview` zeigt keine werkzeugspezifische PingCastle-Engine-Version mehr. | Collapse soll ausschliesslich die Hoehe reduzieren und darf die stabile Spaltengeometrie nicht veraendern. Die Directory-Karte beschreibt den Verzeichniszustand; eine Scanner-Engine-Version gehoert nicht zu diesem fachlichen Ueberblick und bleibt nur in der unveraenderten Provenienz erhalten. |
|
||||
| 2026-09-03 | Das Client-Info-Hauptraster verwendet drei feste Karten-Spalten mit der dauerhaften Zuordnung 3/2/2 und keine dichte Masonry-Neuverteilung mehr. Collapse veraendert nur die Hoehe innerhalb derselben Spalte. Die Desktop-KPI-Matrix streckt ihre sechs Karten auf die Hoehe von `Assessment Status`; ihre feste Reihenfolge ist Compliance Score, Deviation, High Severity, Passed, Typed Observations, Directory Sections. Nur Deviation und High Severity sind Filteraktionen. | Die ausdrueckliche Nutzeranforderung priorisiert stabile raeumliche Orientierung gegenueber global maximaler Verdichtung. Feste Spalten verhindern Positionswechsel beim Minimieren und Maximieren; die gemeinsame Kopfhoehe beseitigt die grosse Leerstelle. Begrenzte Klickziele vermeiden versehentliche Filterwechsel, waehrend die staerkere Compliance-Hierarchie den wichtigsten Gesamtwert hervorhebt. Diese Entscheidung supersediert die unmittelbar folgende Masonry-Entscheidung. |
|
||||
|
|
@ -1862,6 +1969,40 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
|
|||
|
||||
## 11. Risiken
|
||||
|
||||
Purple-Knight-Begleitdateien 2026-09-10: Alte Importe koennen den nativen Wert noch nicht als Kontextbeobachtung enthalten. Dann kann die neue Leseprojektion ihn nicht nachtraeglich aus Rohtext erfinden und die betroffenen Summary-/Findings-Dateien muessen gemeinsam neu importiert werden. Mehrere unterschiedliche Ergebnisberichte in demselben Scanlauf werden bewusst nicht automatisch mit einem einzelnen Summarywert verbunden.
|
||||
|
||||
Weitere Scanner 2026-09-10: Unbekannte Formate und anders skalierte oder mehrdeutige Kennzahlen benoetigen eine belegte Profilzuordnung; beliebige Scorefelder werden nicht automatisch als Gesamtwert interpretiert. Pflichtdeklaration und synthetische Regressionen sichern den Vorrang unterstuetzter nativer Werte, ersetzen aber keine fachliche Pruefung neuer Herstellerformate.
|
||||
|
||||
Scoring 2026-09-10: Der hybride Index ist nicht empirisch zwischen Herstellern kalibriert. Unbekannte Ziel-/Laufidentitaet, fehlende Quellversionen, hybride native Berichte sowie unvollstaendige Pruefpopulationen begrenzen Deduplikation und Vergleich; die Oberflaeche kennzeichnet diese Faelle. Historischer Scorevergleich fuehrt offene Scannerreferenzen nur read-only fort; persistierte Workflow-Fortschreibung, GPO-Historie und auditierter Scope-Austritt bleiben im separaten Phase-8-Arbeitspaket. Historische Kontextlesefehler sperren Fortschrittsaussagen, nicht das Oeffnen des aktuellen Workspace.
|
||||
|
||||
HTML-Timeout-Restgrenze 2026-09-09: Die konkrete Datei aus dem Screenshot wurde aus Datenschutzgruenden nicht geoeffnet. Reproduziert und behoben ist ein gleichartiger Timeout bei langen wertlosen Attributen, zusaetzlich wurden wiederholte Verweissuchen reduziert. Damit ist noch keine erfolgreiche Verarbeitung dieser konkreten Datei belegt; bei erneutem Fehler sind eine synthetische strukturgleiche Reproduktion oder eine ausdruecklich freigegebene minimale Inhaltspruefung erforderlich. Laufzeiten sind lokale Beobachtungen, keine CI-Millisekundengarantie. Fachliche Extraktion und Profile bleiben kompatibel; bestehende erfolgreiche Importe muessen wegen dieser Performancekorrektur nicht umgeschrieben werden, fehlgeschlagene Vorschauen muessen erneut gestartet werden. Keine SQL-/API-/Persistenzaenderung. Der globale Local-only-Check bleibt trotz 1.131 bestandener Tests an der fremden GPO-DisplayName-Aenderung offen; der HTML-Fix ist deshalb nicht als vollstaendig gruene npm-test-Gesamtkette ausgewiesen.
|
||||
|
||||
PurpleKnight-Importgrenzen 2026-09-09 (Phase 13A/13C): Verifiziert sind die freigegebenen CSV-/XLSX-/HTML-Exporte und synthetische Varianten, nicht jede kuenftige Toolversion. Ohne deklarierte Summen ist kein vollstaendiger rechnerischer Bilanznachweis moeglich. Die statische Datengrammatik ist kein allgemeiner JavaScript-Interpreter; neue dynamische Exportvarianten benoetigen eine begrenzte Erweiterung, keine Ausfuehrung. Mehrfachvorkommen und Widersprueche bleiben sichtbar statt heuristisch zusammengefuehrt zu werden. Die gelieferten CSVs und HTML/XLSX stammen aus unterschiedlichen Scanzeitpunkten und duerfen nicht auf identische Statussummen gezwungen werden. Bestehende Importe bleiben unveraendert und benoetigen Reimport fuer neue Erfassungsdaten. AD-Domaenen-Begleitdateien bleiben als Rohquellen erhalten; die neue typisierte HTML-Domaenenliste ersetzt keine allgemeine Inventarmodellierung. Keine Persistenzmigration, optionale PostgreSQL-Integration nicht aktiviert; bestehender Client-Bundlegroessenhinweis bleibt offen.
|
||||
|
||||
PingCastle-Importgrenzen 2026-09-09 (Phase 13A/13C): Verifiziert sind das freigegebene Dummy-Paar und synthetische Varianten, nicht jede PingCastle-Version. Ohne native Zaehler kann kein rechnerischer Vollstaendigkeitsnachweis erfolgen. Doppelte IDs werden diagnostiziert, bleiben aber gemaess bisheriger Entscheidung ein Finding mit erster Darstellung; es findet keine automatische Zusammenfuehrung widerspruechlicher Darstellungen statt. Der bekannte fehlende HTML-Anker und die XML-Results-Einschraenkung bleiben sichtbare Warnungen. Generische XML-Attribute sind keine automatische Assetklassifikation; semantisches XML-Mapping und Control-Path-JSON bleiben offen. Alte Importe benoetigen Reimport. Keine Schema-/Datenmigration, optionale PostgreSQL-Integration nicht aktiviert; vorhandene Client-Bundlegroessenwarnung bleibt bestehen.
|
||||
|
||||
Maester-Importgrenzen 2026-09-09 (Phase 13A/13C): Die statische Datengrammatik und der lexikalische Zuweisungsfinder sind kein allgemeiner JavaScript-Parser. Neue dynamische Exportvarianten benoetigen eine ausdrueckliche Erweiterung mit synthetischen Ablehnungs- und Vollstaendigkeitstests; ihre Ausfuehrung bleibt verboten. Verifiziert sind die bereitgestellten Dummy-Dateien und synthetische Varianten, nicht pauschal jede Maester-Version. Bekannte Zaehlerabweichungen markieren die Verarbeitung mit Warnungen, obwohl saemtliche Einzeltests erhalten bleiben. Bereits gespeicherte Importe erhalten weder die korrigierte Evidenzabbildung noch neuen HTML-Kontext automatisch. Excel und HTML bleiben eigenstaendige Quellen mit eigener Provenienz; gleichzeitiger Import beider Dateien erzeugt weiterhin separate Vorkommen und keine automatische dateiuebergreifende Deduplizierung. Der Maester-Laufzeitrechner darf nicht als bewertetes Zielsystem in die Assetzaehlung gelangen. Keine Persistenz- oder Schemaaenderung; optionale PostgreSQL-Integrationstests wurden nicht aktiviert. Die bestehende Client-Bundlegroessenwarnung bleibt offen.
|
||||
|
||||
Navigationsperformance 2026-09-09 (Phase 7): Die Selektoren setzen unveraenderliche Quellrecords, Blaetter und Eingaben voraus; fachliche Aenderungen brauchen neue Objektidentitaeten. Wiederverwendung und Invalidierung sind getestet, Varianten je Blatt auf vier begrenzt. Zeitwerte haengen von Datenbreite, Browser und konkurrierender Last ab; CI sichert Verhalten und Operationen statt harter Millisekundenlimits. Notwendiger Kalt-/Neuaufbau bleibt teurer: nach Filterreset im Dummy rund eine Sekunde fuer Pathfinder beziehungsweise Client Info. Result DB, DB Structure und Datahub erreichten 34/44/109 ms bis zum dritten Frame; asynchrone Datenbereitstellung ist darin nicht enthalten. Die vorhandene Client-Bundlegroessenwarnung bleibt offen. Vollstaendige Einzelmessungen und Grenzen sind im Performance-Dokument festgehalten.
|
||||
|
||||
GPO-Quellenrisiko 2026-09-08: Bei erneuter Evaluation kann die referenzierte Backupteilmenge wachsen, ohne dass ein neuer Import vorliegt. Die Auswahl ueber autoritative aktuelle Evaluationen und die stabile Aggregations-ID verhindert dadurch erzeugte Doppelzaehlungen. Ohne belegbaren Importzeitpunkt bleibt `Imported` leer und die Quellenbezeichnung neutral `GPOs`; der Laufzeitpunkt wird nicht als Importdatum erfunden. Der zusaetzliche projektgebundene Read-Aufruf darf bei einem Fehler nicht still auf Historie zurueckfallen. Synthetische Regressionen sichern 2-zu-3-Backup-Abdeckung, Supersession, getrennte echte Importe, Timeslotgrenzen, fehlende Kundenwerte, Konsistenzzeilen und unveraenderte Scannerzeilen. Keine automatische Reconciliation wird fuer diese reine Leseaenderung ausgeloest.
|
||||
|
||||
Security-Template-Risiko 2026-09-08: Ein partiell verarbeitetes INF darf nicht als vollstaendig interpretiert werden; ausgelassene Eintraege sind keine belegte Abwesenheit. Diagnosecodes, `partial` und `inventory-only` bleiben deshalb entlang der Pipeline sichtbar. Vorhandene Baselineanalysen werden bis zur versionierten Neuanalyse nicht durch den Parserfix korrigiert; vorhandene Evaluationen benoetigen danach einen neuen Lauf. Die vom Nutzer genannten 109/109 beziehungsweise 107 Deviations plus 2 Review sind eine noch zu verifizierende Erwartung; wiederhergestellte Baselinevarianten koennen weitere Consensus-Konflikte aufdecken. Die bestehende Job-Reconciliation plant bei einer neuen GPO-Analyserversion projektuebergreifend veraltete Quellen neu ein. Beim ersten Neustart entstand dadurch unbeabsichtigt ein zusaetzlicher Evaluationslauf ausserhalb des freigegebenen Dummy-Scope; Originale und Historie blieben erhalten, Backend wurde angehalten und keine weiteren aktiven Jobs festgestellt. Dieser automatische Versionspfad ist bei kuenftigen projektbegrenzten Betriebsaktionen vor einem Neustart explizit zu beruecksichtigen. Inhaltspruefungen ausschliesslich synthetisch, Livekontrollen nur mit anonymisierten technischen Zaehlern.
|
||||
|
||||
Festgelegtes Client-Info-Darstellungs- und Bewertungsrisiko vom 2026-09-07: Eine globale Sondergestaltung koennte andere Ansichten veraendern; native Tool-Scores koennten mit dem ASSESSOR Overall Score vermischt werden; heuristische Domain-Linsen koennten ueberlappende Findings wie unabhaengige Populationen erscheinen lassen; fehlende Environment-Fakten koennten als Null fehlinterpretiert werden. Gegenmassnahme: alle Sonderfarben und Geometrien ausschliesslich unter `.client-command-center` und `--client-info-*` kapseln, den Overall Score nur aus vergleichbaren ASSESSOR-Quellenscores bilden, native Metriken nur beschriftet in Source Overview zeigen, Domainzuordnung als rein praesentative Multi-Label-Linse ohne Einfluss auf Overall Score behandeln und unbekannte Fakten konsequent als Gedankenstrich anzeigen. Synthetische Regressionen pruefen Scoreaggregation, acht Domains, Source-Komposition und fehlende Werte; Live-Sichtpruefung bleibt auf den freigegebenen Dummy begrenzt.
|
||||
|
||||
Festgelegtes Workspace-Navigationsrisiko vom 2026-09-07: Ein aus dem aktiven Fachblatt abgeleiteter oder gegen dessen Treffer nachgezaehlter Source-Filter kann alle anderen Scanner ausblenden; ein blosser Fremdwertfilter auf dem GPO-Blatt wuerde wiederum eine leere Ansicht statt des gewuenschten Scans erzeugen. Gegenmassnahme: Optionsliste ausschliesslich aus dem vorhandenen projektweiten Aggregat bilden, die stabile Provenienz-ID von Spaltenindizes entkoppeln, ihre Werte von lokaler Nachzaehlung ausnehmen und bei einer Auswahl aus einem Fachblatt zuerst auf dasselbe Aggregat wechseln. Regressionen pruefen drei unterschiedliche Scans, unveraenderte globale Optionen und das Navigationsziel.
|
||||
|
||||
Festgelegtes Facettenrisiko vom 2026-09-07: Die allgemeine Einwertigkeitspruefung oder ein kuenftiges Facettenlimit koennte `Source: Scan` erneut aus GPO- oder anderen Ansichten entfernen; flexible Gruppenbreite koennte den Filter ausserdem optisch zwischen Fachfacetten statt stabil ganz rechts platzieren. Gegenmassnahme: Source-Scan fachlich explizit erkennen beziehungsweise aus vorhandener Provenienz ableiten, von der Nuetzlichkeitsunterdrueckung ausnehmen, nach dem begrenzten Fachfacettensatz als letztes Element anfuegen und per eigener CSS-Klasse mit automatischem linken Rand an die rechte Kante setzen. Synthetische Tests pruefen GPO-Vollreihenfolge, Fallback, Klasse und Endposition.
|
||||
|
||||
Festgelegtes GPO-Quellenpfadrisiko vom 2026-09-07: Source-Locators koennen lokale Laufwerke, Backupordner, Artefaktnamen oder kundenirrelevante Importstruktur vor der eigentlichen GPO-GUID enthalten; eine tolerante Teilanzeige koennte diese Informationen offenlegen. Gegenmassnahme: zentraler reiner Presenter mit geschlossener GUID-/`DomainSysvol/GPO`-Erkennung, Anzeige ausschliesslich ab dem erkannten internen Anker und fail-closed Platzhalter fuer jede andere Form. Originalprovenienz bleibt unveraendert gespeichert und wird nicht zur Navigation oder Dateiaufloesung verwendet.
|
||||
|
||||
Festgelegtes Duplicate-Praesentationsrisiko vom 2026-09-07: Eine vereinfachte Duplicate-Anzeige koennte Quellenredundanz faelschlich als Deviation darstellen oder umgekehrt eine zusaetzliche echte Baselineabweichung als unkritisch erscheinen lassen. Gegenmassnahme: ausschliesslich die identische Mehrquellenbelegung beschreiben, explizit `A duplicate alone is not a baseline deviation` formulieren, keine Pass-/Failfarbe verwenden und Duplicate-/Baselineklassifikation im Domainmodell weiterhin als getrennte Aussagen behandeln.
|
||||
|
||||
Festgelegtes Tabellenrisiko vom 2026-09-07: Mehrere Varianten, lange Pfade oder grosse Wertstrings koennen die gemeinsame Vergleichstabelle unlesbar verbreitern oder Inhalte abschneiden. Gegenmassnahme: semantische Mindestbreiten nur innerhalb eines eigenen horizontal rollbaren Tabellencontainers verwenden, alle Zellen an beliebigen sicheren Stellen umbrechen lassen, keine Clamps/Ellipsen einsetzen, Quellen pro Variante bedarfsgesteuert expandieren und die vollstaendige technische Provenienz in der nachgelagerten Detailstufe erhalten.
|
||||
|
||||
Festgelegtes Praesentationsrisiko vom 2026-09-07: Eine vereinfachte Konfliktansicht koennte leere/Whitespace-Werte normalisieren, technische Provenienz verlieren oder durch Farb- und Sprachwahl faelschlich eine wirksame beziehungsweise bevorzugte Variante behaupten. Gegenmassnahme: Karten ausschliesslich aus dem vollstaendig validierten begrenzten Evidence-Envelope ableiten, Originalzellwerte unveraendert erhalten, leere und Whitespace-Werte explizit kennzeichnen, alle Quellen/IDs unter technischen Details bewahren, nur den neutralen Warning-Akzent verwenden und bei jeder Mehrdeutigkeit auf die bestehende inerte Tabellenprojektion zurueckfallen.
|
||||
|
||||
| Die Produktversion bleibt trotz funktionaler Aenderungen unveraendert oder wird mit einer technischen Komponentenfassung verwechselt; Frontend und Backend koennen zudem unterschiedliche Builds ausliefern. | Produktversion zentral aus dem Paket beziehen, im Markenblock zeigen, Buildzeit und Backend-Release im About-Popup ausweisen und die Backend-Produktversion ueber Health melden. Releaseprozess und Tests muessen die bewusste SemVer-Aenderung pruefen. |
|
||||
|
||||
| Leere oder technisch nicht normalisierbare Microsoft-Baselinewerte koennten als Abweichung erscheinen und die Work-Liste mit nicht belastbaren Findings aufblasen. | Baselinezeilen ohne Sollwert vor der Evaluation ausschliessen; Pass/Fail nur bei vorhandenem vergleichbarem Baselinewert zulassen; nicht unterstuetzte Werte als Review/Warning fuehren und synthetisch sowie per inhaltsfreier Zaehlerpruefung absichern. |
|
||||
|
|
@ -2094,6 +2235,28 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek
|
|||
|
||||
## 12. Änderungsprotokoll
|
||||
|
||||
Verifiziert 2026-09-10 (Phase 7/13C, Purple-Knight-Scorezuordnung): Separate native Summary und eindeutige Findings desselben Scanlaufs werden sicher verbunden. Regression 65 native/96 Passed/19 Deviations ergibt 65 Native score in der Dashboardprojektion. 1.199 Tests, Sicherheits-/Designchecks und beide Produktionsbuilds erfolgreich. Keine Datenmutation oder Realdateninspektion.
|
||||
|
||||
Verifiziert 2026-09-10 (Phase 6/13C/7, weitere Scanner): Native-first-Regel in verpflichtende Profildefinitionen und gemeinsamen Katalogpfad ueberfuehrt. Zehn neue Regressionen; 1.198 Tests, Sicherheits-/Designchecks und beide Produktionsbuilds erfolgreich. Keine Migration, Reimportmutation oder Backendneustart.
|
||||
|
||||
Verifiziert 2026-09-10 (Phase 7/13C/8, Hybridscore): Native Beitraege mit berechnetem Fallback, feste Quellgewichte, genaue Provenienz, Lauf-Deduplikation, Domain-Pass-rates und Methodikdialog implementiert. Zeitvergleich trennt gemeinsame aktuelle Evidenz und historische offene Scannerverpflichtungen. 1.188 Tests, Local-only-/Designchecks und beide Produktionsbuilds erfolgreich; kleine Ergaenzung der vorhandenen Testfixture-Typdeklaration beseitigt TS7016. Keine SQL-Migration oder Realdatenmutation; verbleibende Phase-8-Workflowgrenzen explizit dokumentiert.
|
||||
|
||||
Verifiziert 2026-09-10 (Phase 7): Source-Auswahl bleibt in Client Info bzw. der bestehenden Work-Unteransicht. Dashboard-Ergebniszahlen folgen dem Source-Filter; Scan All, Timeslotwechsel, Quellenauswahl im Cache und unveraenderter Wiederbesuch synthetisch geprueft. 1.157 Tests, Local-only-/Designpruefung und Clientbundle erfolgreich; Gesamtbuild am vorbestehenden TS7016 der HTML-Testfixture blockiert. Keine Datenmutation oder Phasenstatusaenderung.
|
||||
|
||||
Verifiziert 2026-09-10 (Phase 7, PurpleKnight): Native HTML-/CSV-Statusunterschiede im freigegebenen read-only Abgleich bestaetigt; keine Anpassung gespeicherter Ergebnisse. Client Info schliesst Not Relevant aus Assessment Gaps und Coverage-Nenner aus und erhaelt Not selected/Canceled als nicht ausgefuehrt. Vier neue Regressionen; 1.152 Tests, Local-only-/Designpruefung und Clientbundle erfolgreich. Gesamtbuild am vorhandenen TS7016 der HTML-Testfixture blockiert. Keine Migration, kein Reimport, Phasenstatus unveraendert.
|
||||
|
||||
Verifiziert 2026-09-09 (Phase 13/13A, HTML-Timeout): Linearer Attributleser und importlokaler Verweisindex beseitigen den synthetisch reproduzierten 30-Sekunden-Abbruch. Derselbe 3.159.338-Byte-Bericht liefert im gebauten Worker alle 40 Regeln/Detailtabellen in 1.590 ms bei unveraenderten Sicherheitslimits. Neun neue Regressionen; 1.131 Tests bestanden und acht PostgreSQL-Tests uebersprungen, beide Builds sowie separate Designpruefung erfolgreich. Die nachgeschaltete globale Sicherheitspruefung bleibt an einem unabhaengig geaenderten GPO-Metadatenparser offen; kein Eingriff in diese fremde Aenderung. Kanonischer Datastore bestaetigt, aktualisiertes Backend und App/Proxy HTTP 200, synthetischer Parse-only-HTTP-Check ohne Revisionsaenderung. Originaldatei aus dem Screenshot nicht gelesen, keine Importpersistenz oder Schemaaenderung. Vorgaben aktualisiert; temporaere Diagnosepruefungen entfernt, dauerhafter Workerlasttest dokumentiert. Phasenstatus unveraendert.
|
||||
|
||||
Verifiziert 2026-09-09 (Phase 13A/13C): PurpleKnight-Auditkorrekturen mit Profil 1.3.0 umgesetzt. 25 neue synthetische Tests sichern Workbook-Komposition, HTML-Kontext, native Feldpfade, volle Zeitpraezision, defekte/mehrdeutige Payloads, Summenabgleich, Null-/Leerfaelle und sichere CSV-Erkennung. Gesamtpruefung: 1.122 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen, Sicherheits-/Designcheck und beide Produktionsbuilds erfolgreich. Dummy: je 119 Ergebnisse fuer CSV/XLSX/HTML; 19 beziehungsweise 20 eindeutig verknuepfte Detailtabellen; HTML 45 Kontextbeobachtungen, 4.743 Originalfelder und 213 Detailzellen exakt. Parse-only-Abgleich aller 24 Dateien sowie finaler Drei-Format-Backendcheck erfolgreich, Repositoryrevision unveraendert. Backend verwendet weiterhin den verifizierten kanonischen Datastore. Keine Migration oder Reimportmutation. AGENTS.md, SECURITY.md, Importgrenzen und Pruefbericht aktualisiert; temporaere Pruefdateien entfernt. Phasenstatus unveraendert.
|
||||
|
||||
Verifiziert 2026-09-09 (Phase 13A/13C): PingCastle-Auditpunkte 1-3 umgesetzt und auf Profil 1.5.0 / Extractor 2.2.0 versioniert. Exakte XML-Attribute/Leere, HTML-Attributgrenzen und native ID-/Zaehlerdiagnosen mit 21 neuen Regressionen abgesichert. 1.096 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen, Local-only-/Designcheck und Client-/Serverbuild erfolgreich. Dummy: beide Formate 37 Regeln; XML 403 Attribute und 70 leere Blaetter vollstaendig, HTML 125 Tabellen und 1.513 Kontextbeobachtungen ohne Attributreste. Parse-only im neu gestarteten Backend bestaetigt Zahlen und unveraenderte Repositoryrevision; kanonischer PostgreSQL-Container gesund, App/Backend/Proxy HTTP 200. Kein Reimport oder Persistenzeingriff. AGENTS.md und Sicherheitsvorgaben ergaenzt; historischer Pruefbericht um verifizierten Umsetzungsstand erweitert, temporaere Pruefdatei entfernt. Auditpunkte 4-5 und Phase-13C-Abnahme bleiben offen.
|
||||
|
||||
Verifiziert 2026-09-09 (Phase 13A/13C): Maester-HTML-Objektliterale sicher und ohne Ausfuehrung eingelesen, Excel-Result/Status kontextabhaengig getrennt, Originale und Berichtskontext erhalten, native Zaehler gegen Einzeltests validiert und Client-Info-Kontext um getrennte Maester-Fakten erweitert. Maester-Verarbeitungsmanifest 1.1.0 mit Reimportempfehlung, unveraenderter Historie und kompatibler Normalisierung. 47 neue synthetische Regressionen; Gesamtlauf 1.075 bestandene Tests, acht optionale PostgreSQL-Tests uebersprungen, Sicherheits-/Designpruefung und beide Builds erfolgreich. Dummy unabhaengig feldweise und anschliessend ueber den laufenden Parse-only-HTTP-Pfad geprueft: Excel/HTML je 318 Tests und 297 Evidenzen, HTML 39 Kontextwerte, zwei explizite Zaehlerwarnungen. App/Backend/Proxy HTTP 200, kanonischer PostgreSQL-Datastore bestaetigt, Repositoryrevision waehrend der HTTP-Pruefung unveraendert. Keine Migration oder Speicherung von Importdaten. Schutz- und Vollstaendigkeitsvorgaben in AGENTS.md, SECURITY.md und docs/IMPORT_SECURITY_LIMITS.md ergaenzt; temporaere Auditdateien entfernt.
|
||||
|
||||
Verifiziert 2026-09-09 (Phase 7): Bereichswechsel beschleunigt und Performance-Vorgaben in AGENTS.md sowie docs/architecture/navigation-performance.md verankert. Gemeinsame Ableitungen, lazy Rohtexte/Exporte/Blattoptionen, begrenztes erstes Rendering und pausierter versteckter Graph. 1.024 Tests, Local-only-/Designpruefung und beide Builds erfolgreich; acht optionale PostgreSQL-Tests uebersprungen. Dummy-Work: 5.324 ms initial beobachtet, abschliessend 121-211 ms bei unveraenderten Wiederbesuchen. Native Auswahl weiterhin vollstaendig, versteckte Graphfilter bei Reaktivierung aktuell; keine gezielte Datenmutation.
|
||||
|
||||
Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client Info. Aktuelle Evaluationen aus dem vorhandenen autoritativen Readmodell statt ergebnisabhaengiger Backupgruppierung; Import- und Auswertungszeit getrennt, Source-Scan-Provenienz auch auf GPO-only-Blaettern. 1.014 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Dummy lesend bestaetigt: eine statt drei GPO-Quellenoptionen, 3.805 aktuelle Zeilen, elf historische Evaluationen unveraendert. Keine Persistenzmutation oder Neuanalyse.
|
||||
|
||||
| Datum | Änderung |
|
||||
| 2026-09-11 | Phasen 2/6/7/11 um den eigenen Schwachstellen-Ergebnistyp erweitert: `vulnerability_finding` ist eine additive ASSESSOR-Erweiterung des unveraenderten externen Result Contract 1.0.0. Das Defender-Profil 1.1.0 setzt fuer statuslose CVE-Findings deklarativ `outcome=warning`; Work und Unified Result projizieren dadurch `result_type=vulnerability_finding`, `is_ioe=false` und `deviation=true`. Die bereits angewandte Migration 022 bleibt checksum-identisch und erweitert nur den geschlossenen PostgreSQL-Constraint; die nachtraeglich benoetigte Aktualisierung bestehender abgeleiteter Result-Type-/Outcome-Spalten liegt append-only in Migration 023. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Profil 1.0.0 bleibt wegen der geaenderten kanonischen Semantik inkompatibel und benoetigt fuer einen vollstaendig aktuellen Datensatz Reimport/Reprocessing. 787 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phasen 2, 6, 7 und 11 bleiben abgeschlossen. |
|
||||
| 2026-09-11 | Phase-7-Abweichungsfilter zwischen Work und Result DB vereinheitlicht: Work prueft `deviation` nicht mehr gegen beliebige profilbezogene Rohwertmuster wie Defender-Severity `Critical`, sondern rekonstruiert aus Werkzeug und Statusfeld dieselben kanonischen Achsen Outcome, Result Type, Exposure State und `is_ioe` und verwendet anschliessend das gemeinsame versionierte Abweichungspraedikat. Defender-CVEs ohne Status bleiben damit wie in der Result DB `outcome=unknown`, `is_ioe=false`, `deviation=false`; Fail-/Warning-Ergebnisse und festgestellte Expositionen bleiben Abweichungen. Keine Daten-, API-, Schema-, Persistenz- oder Profilversionsaenderung. 781 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phase 7 bleibt abgeschlossen. |
|
||||
|
|
@ -2111,6 +2274,19 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek
|
|||
| 2026-09-10 | Phasen 7/13A fuer gemischte Defender-Timeslots korrigiert: Work bestimmt den Titel jedes Datensatzes anhand seiner Scannerprovenienz und des deklarativen Titel-Headers, sodass Defender-Vorkommen im gemeinsamen All-Records-Blatt die CVE statt Device-Name oder IP zeigen. Client Info behaelt bei mehreren Scannern die allgemeine Uebersicht und bietet Defender zusaetzlich als eigenen Tab mit den vorhandenen CVE-, Device- und Site-Sichten an. 19 fokussierte Dashboard-, Titel- und Graphregressionen erfolgreich. Keine API-, SQL-, Profil- oder Persistenzmigration; Phasen 7 und 13A bleiben abgeschlossen. |
|
||||
| 2026-09-10 | Phase-13A-Verarbeitungsvertrag fuer Defender-Importe korrigiert: Die Kompatibilitaetspruefung verwendet nun den tatsaechlich persistierten `processedWith.profileId` statt einer eventuell abweichenden erkannten Werkzeug-ID. Dadurch werden registrierte Defender-Profile 1.0.0 als aktuell bewertet; unbekannte Profil-/Versionspaare bleiben fail closed. Die Korrektur ist read-only fuer vorhandene Importdaten und benoetigt keinen Reimport oder Datenbankeingriff. 17 fokussierte Import-/Kompatibilitaetstests sowie der Gesamtlauf mit 766 bestandenen und 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich; Phase 13A bleibt abgeschlossen. |
|
||||
| 2026-09-10 | Microsoft Defender for IoT vulnerability imports and views umgesetzt: Profil und Source Extractor 1.0.0 erweitern die generische Excel-/CSV-Pipeline; Device-Identitaeten, CVE-Definitionen, Device-mal-CVE-Vorkommen, rohe Sensorbeobachtungen und Aggregate-only-Zahlen bleiben getrennt. Client Info bietet CVE-, Device- und Site-Sichten mit Evidenz- und Reconciliation-Drill-down. Summary-only-Quellen bleiben ohne erfundene Findings viewable. 33 fokussierte synthetische Tests sowie 764 Standardtests, 8 uebersprungene optionale PostgreSQL-Tests, Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Keine API-, SQL- oder Persistenzmigration; Phasen 6, 7 und 13A bleiben abgeschlossen. |
|
||||
| 2026-09-08 | Phase 7: Verbindungskurven der Client Info treffen ihre Domainknoten wieder. Feste Zielkoordinaten in einem verzerrten SVG-Koordinatensystem durch eine aus dem Layout gemessene Geometrie ersetzt, Neumessung ueber Groessenbeobachter und Fensterereignis. Am Dummy bei drei Breiten geprueft: Abweichung 0 Pixel statt bis zu 80. Keine Aenderung an Werten oder Kachelgeometrie. 1.011 Tests, Local-only-/Designpruefung und Builds erfolgreich. |
|
||||
| 2026-09-08 | Phase 7: Aufbauzeit der Client Info von ueber vier Sekunden auf unter 100 Millisekunden gebracht. Ableitung je Datensatz einmal statt je Rendern, ein Durchlauf statt vierzehn Punkteberechnungen, Domainzuordnung mit einmaligem Klassifikationstext, schwache Verweise je Datensatz sowie ansichtsuebergreifender Zwischenspeicher fuer Facetten und Auswertung; Herkunftswerte je Datensatzliste einmal gezaehlt. Gemessen am freigegebenen Dummy mit 8.257 Datensaetzen: Ableitung 3.123 ms -> 0 ms, laengste blockierende Aufgabe > 4.000 ms -> < 100 ms. Keine Aenderung an Werten, Zaehlungen, Darstellung oder der geteilten Statuslogik. 1.007 Tests, Local-only-/Designpruefung und Builds erfolgreich. |
|
||||
| 2026-09-08 | Phase 7/13C: Dummy-Reimport abgeschlossen. Purple-Knight-Scan nach Backendneustart ueber den Importdialog ersetzt; alle aktiven Scans tragen den aktuellen Verarbeitungsstand (Purple Knight 1.2.0, CIS-CAT 1.4.0, PingCastle 1.4.0, Maester 1.0.0), Datahub meldet durchgehend `Ready`. Befunde und Umgebungskontext unveraendert; keine Code-, Schema- oder Persistenzaenderung. |
|
||||
| 2026-09-08 | Phase 7/13C: Freigegebenen Dummy ueber den regulaeren Reimportweg mit identischen Originaldateien neu importiert (verifiziertes Backup vorab, SHA-256-Gleichheit aller 26 Dateien, Befundzahlen unveraendert, abgeloeste Laeufe als Historie erhalten). CIS-CAT-Kontext auf die tatsaechliche Berichtstitelseite umgestellt: Host, Zielnetzadresse, Benchmark mit Version, Profil, Zeitpunkt, Dauer und Assessor-Version; lesbarer Benchmarkname auch aus einer XCCDF-Ergebniskennung. Purple-Knight-Profil 1.2.0 und CIS-CAT-Profil 1.4.0 als kontextbezogene, kompatible Verarbeitungsstaende registriert. Client Info zeigt fuer den Dummy 49 Faktenzeilen ohne unbekannte Werte. 1.004 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Offen: Backendneustart und ein erneuter Purple-Knight-Reimport, damit die bestaetigungspflichtige Indicators-Datei die aktuelle Profilversion traegt. |
|
||||
| 2026-09-08 | Phase 7/13C: Purple-Knight-Zusammenfassung und CIS-CAT-Kopfangaben als Umgebungskontext erschlossen. Neuer kontextreiner Purple-Knight-Extractor (Posture Score, sechs Kategoriewerte, Auswertungsbilanz, Edition, Forest, Domainzahl) ohne Findingkandidaten, zwei neue Inhaltssignale fuer die Quellenerkennung; CIS-CAT-Extractor 1.3.0 mit beschrifteten Systemangaben, Benchmarkidentitaet, Gesamtscore, deklarierten Ergebniszahlen und Abschnittsscores. Client Info erhaelt die Zone `Environment Detail`, Domainknoten mit vergleichbarem ASSESSOR-Score, Quellkacheln mit Score zuerst und beschrifteter Toolzahl daneben sowie die GPO-Anzahl aus der Sicherungsverarbeitung. Unbekanntes bleibt `—`, lizenzbeschraenkte Abschnitte bleiben ausdruecklich Restricted. 1.003 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Keine Schema-, Migrations- oder Persistenzaenderung; neue Beobachtungen entstehen erst bei erneutem Import. Ausschliesslich synthetische Inhaltspruefung. |
|
||||
| 2026-09-08 | Phase 13B: Security-Template-Parser 1.1.0 ueberspringt unbekannte/metadatenhaltige Abschnitte vor der Key/Value-Grammatikpruefung und erhaelt gueltige Instruktionen bei isolierten defekten Eintraegen mit Status partial. Struktur- und Ressourcengrenzen bleiben blockierend. Baseline-Analyzer 1.2.0, GPO-Processing/Analyzer 2.2.3 und analysegebundene ADMX-Katalog-IDs ermoeglichen versionierte Neuanalyse mit erhaltener Historie. Synthetische Regressionen pruefen Service-Zeilen, Einzelfehler, leere Rechtezuweisungen, Quellzeilen, Strukturgrenzen, gemeinsame Kunden-/Baseline-Pipeline, Legacy-Reprocessing, Idempotenz und Katalogreferenzen. 990 Tests, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich; acht optionale PostgreSQL-Tests uebersprungen. Backup verifiziert; elf Baselinepakete neu analysiert, alte Analysen erhalten, Privilege Rights 146 -> 400. Dummy-Evaluation fuer konfigurierte Kundenwerte: 113 Matches, 98 Abweichungen, 15 Baseline-Consensus-Reviews; 28 neue Rechteabweichungen, 13 bisherige Abweichungen -> Review, zwei alte Reviews unveraendert. Die bestehende automatische projektuebergreifende Job-Reparatur erzeugte beim ersten Neustart unbeabsichtigt einen zusaetzlichen Evaluationslauf ausserhalb von Dummy; im Risikoabschnitt dokumentiert, Originale/Historie erhalten, danach keine weitere fremde Evaluation. HTTP-Health direkt und via Client erfolgreich. Keine SQL-Migration, Phase bleibt in Arbeit. |
|
||||
| 2026-09-07 | Phase 7/13B: GPO-Konsistenzklassifikation in der Result DB ergaenzt. Baselinevergleiche tragen Misconfigured, Not configured, Match, Conflict oder Review required; identische beziehungsweise abweichende Mehrquellenbelegungen werden ueber dieselbe aktuelle Policy-Beobachtungsgruppierung wie Work als eigene Duplicate-/Conflict-Leseprojektionen bereitgestellt. Neue Facette, Queryparameter und Detailanzeige gelten identisch fuer Local und PostgreSQL. Conflict/Duplicate bleiben Outcome warning und ausdruecklich `Deviation: No`; die Facette bleibt trotz Standard-Deviationfilter sichtbar und hebt diesen bei Klassenauswahl auf. Keine Persistenz-/Schemaaenderung, Migration, Neubewertung oder Originaldatenmutation. 976 Tests bestanden, 8 optionale PostgreSQL-Integrationstests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Ausschliesslich synthetische Inhaltspruefung. |
|
||||
| 2026-09-07 | Phase 7: `Source: Scan` von einer blattlokalen Facette zur workspaceweiten Scannavigation erweitert. Die vollstaendige Liste stammt aus dem projektweiten All-Records-Blatt und bleibt auch in GPO sowie unter anderen aktiven Filtern vollstaendig. Eine konkrete Auswahl aus einem dedizierten Fachblatt navigiert auf das Aggregat und setzt dort atomar den stabilen Provenienzfilter; die Quellspaltenposition ist fuer die Filter-ID und Wertauflosung nicht mehr relevant. 33 fokussierte synthetische Filter-/GPO-Regressionen gruen; 971 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; keine Daten-, API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-07 | Phase 7: Filterleiste der reinen GPO-Ansicht auf die geforderte stabile Ordnung gebracht. Die fachlichen Controls ruecken ohne kuenstlichen Flex-Zwischenraum nach links; `Source: Scan` bleibt als letzter Filter in jeder Ansicht ganz rechts. Einwertige Source-Scan-Facetten werden nicht mehr verworfen, fehlende explizite Felder erhalten einen praesentativen Provenienzfallback, und das GPO-Limit wurde auf elf sichtbare Facetten angehoben, damit kein Fachfilter verdraengt wird. 32 fokussierte synthetische Filter-/GPO-Regressionen gruen; 970 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; keine Daten-, API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-07 | Phase 7/13B: Kundenanzeige aller GPO-Sources auf GPO-interne Referenzen minimiert. Ein zentraler Presenter entfernt Laufwerks-, lokale Ordner-, Backup- und Artefaktpraefixe und gibt nur `{GUID}/DomainSysvol/GPO/...` beziehungsweise den sicher erkannten internen Strukturteil aus; unbekannte Formen liefern fail-closed keinen Originalpfad. Normale Sources und Conflict-/Duplicate-Evidence verwenden denselben Vertrag. Evidence zeigt jede Referenz sofort in der technischen Pfadschrift, ohne `Show source`, fette Wiederholungsueberschrift oder Klickpflicht. Originaldaten/Persistenz unveraendert; technische Backup-/Record-IDs bleiben eingeklappt. Vier Source-, elf Conflict- und neun Duplicate-Regressionen sowie 36 fokussierte Tests gruen; 967 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; ausschliesslich synthetische Daten, keine API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-07 | Phase 7/13B: GPO-Duplicate-Results an die neue Conflict-Lesepipeline angeglichen. `Duplicate detected` erklaert identische Mehrquellenbelegung und grenzt sie von einer Baselineabweichung ab; danach folgen `Setting details`, eine gemeinsame `Observed value`-Tabelle mit Value, Type und Evidence sowie `Technical source details`. Quellen bleiben direkt in der Evidence-Zelle aufklappbar, vollstaendige Pfade/IDs und exakte Leer-/Whitespacewerte erhalten. Shared Renderer/Envelopeprojektion unterstuetzt nun Conflict und Duplicate, sicherer Tabellenfallback unveraendert. Neun Duplicate- und elf Conflictregressionen, 36 fokussierte Resulttests gruen; 967 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; ausschliesslich synthetische Daten, keine Daten-, API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-07 | Phase 7/13B, unmittelbare Nutzerkorrektur der GPO-Conflict-Results: Lesereihenfolge auf `Conflict detected` -> `Setting details` -> `Observed variants` -> `Technical source details` festgelegt. Die zuvor eingefuehrten Variantenkarten durch eine gemeinsame responsive Tabelle mit Variant, Value, Type und Evidence ersetzt; Quellen bleiben in der jeweiligen Tabellenzeile aufklappbar. Vollstaendige Umbrueche ohne Clipping/Truncation, lokale horizontale Tabellenrollflaeche bei schmalen Fenstern und technische IDs als letzte eingeklappte Stufe. Neutralitaet, exakte Leer-/Whitespacewerte, Envelopevalidierung und Tabellenfallback unveraendert. Elf Konfliktregressionen und 34 fokussierte Resulttests gruen; 965 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; ausschliesslich synthetische Daten, keine Daten-, API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-07 | Phase 7/13B: GPO-Conflict-Results von zwei generischen Tabellen auf eine erklaerende Vergleichsansicht umgestellt. Kurzer Settingtitel, Scope und neutraler Konflikthinweis stehen vor responsiven, nicht gestreckten Variantenkarten mit exaktem Wert, lesbarem Registrytyp, Quellenanzahl und direkt zugeordneten aufklappbaren Quellen. Leere Strings und reine Whitespacewerte sind sichtbar bezeichnet; Registrykontext steht einmal separat, vollstaendige Source paths sowie interne Backup-/Record-IDs bleiben unter `Technical source details` erhalten. Keine Gewinner-/Praezedenzbehauptung, keine Rot-/Gruenwertung. Die strukturierte Sicht wird erst nach vollstaendiger Envelopevalidierung aktiviert; sicherer Tabellenfallback und Duplicate-Results bleiben unveraendert. Zehn Konfliktregressionen und 33 fokussierte Resulttests gruen; 964 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Bestehender Bundle-Groessenhinweis; ausschliesslich synthetische Daten, keine Daten-, API-, Persistenz- oder Schemaaenderung und keine Realdateninspektion. |
|
||||
| 2026-09-06 | Phase 7: GPO-Work-Kacheln gegen ueberlappende lange Registry- und Policy-Pfade abgesichert. `group-policy`-Kacheln tragen eine explizite Toolkennung und erlauben im Titel sichere Umbrueche innerhalb ungeteilter Pfadsegmente; Titel bleiben in ihrer mittleren Gridspalte und ueberdecken weder Bookmark noch Nachbarkachel. Andere Findingtitel behalten die bestehende wortbasierte Typografie. 42 fokussierte synthetische Tests, Designpruefung und Client-Produktionsbuild erfolgreich; keine Daten-, API-, Persistenz- oder Schemaaenderung. |
|
||||
| 2026-09-06 | Phase 7/13B: GPO-only-Timeslotfreigabe auf alle Clientansichten vereinheitlicht. Die zentrale Scope-Regel akzeptiert mindestens eine Scanner-Run-ID oder Assessment-ID; Client Info, Work, Result DB und Pathfinder behandeln damit reine GPO-Timeslots als ausgewaehlt, waehrend Datahub und DB Structure scannertypunabhaengig bleiben. Result-DB-Laden und -Export verwenden denselben projekt- und assessmentgebundenen Scope. Kein automatisches Beimischen anderer Timeslots oder Scannerlaeufe. 25 fokussierte synthetische Tests, Designpruefung und Client-Produktionsbuild erfolgreich; keine Daten-, API-, Persistenz- oder Schemaaenderung. |
|
||||
| 2026-09-06 | Phase 7: Englisch als verbindliche Sprache fuer saemtliche nutzerseitig sichtbaren ASSESSOR-Texte festgelegt, einschliesslich Navigation, Labels, Aktionen, Status-, Lade-, Leer-, Fehler-, Bestaetigungs- und Dialogtexte sowie kuenftiger Funktionen. Deutsche Texte bleiben auf interne Dokumentation, Kommentare oder ausdruecklich angeforderte Exportinhalte begrenzt. Den verbliebenen deutschen Ladehinweis der Data-Hub-Historie auf `Loading clients …` umgestellt. Keine Daten-, API-, Persistenz- oder Schemaaenderung. |
|
||||
|
|
|
|||
|
|
@ -50,12 +50,93 @@ Jeder Auftrag erhält einen neuen Worker. Nach Ablauf des Zeitlimits wird dieser
|
|||
|
||||
## Formatspezifische Härtung
|
||||
|
||||
- HTML: `script`, `style`, eingebettete Frames/Objekte, Formulare, Medien, SVG und weitere aktive Elemente werden entfernt. Ereignisattribute und navigierende oder nachladende Attribute wie `src`, `href`, `srcdoc` und `action` werden vor der Tabellenauswertung gelöscht. Importierter Code wird nicht ausgeführt. Berichte oberhalb von 4 Mi Zeichen sowie DOM-Stacküberläufe wechseln automatisch in eine begrenzte, nicht rekursive Tabellenextraktion.
|
||||
HTML-Performancefix 2026-09-09: Formatadapter 1.1.1, PingCastle-Extractor
|
||||
2.2.1 und CIS-CAT-Extractor 1.3.1 verwenden einen gemeinsamen linearen
|
||||
Attributleser. Lange Attribute ohne Wert und lange Leerraeume duerfen keine
|
||||
quadratischen Suchwiederholungen verursachen. Interne PingCastle-IDs und
|
||||
Ankernamen werden fuer das jeweilige Markup einmal indiziert; Duplikate,
|
||||
Reihenfolge und Offsets bleiben erhalten. Profile und fachliche Mappingvertraege
|
||||
bleiben unveraendert. Der rein synthetische 3.159.338-Byte-Regressionstest
|
||||
brach im alten Produktionsworker nach 30.011 ms ab und laeuft nach dem Fix
|
||||
mit allen 40 Regeln und 40 verknuepften Tabellen in beobachteten 1.590 ms.
|
||||
Die Messung ist kein universelles Laufzeitversprechen. Wiederholbar nach dem
|
||||
Build mit `node tests/server/html-parser-stress.mjs`; der Test behaelt das
|
||||
30-Sekunden-Budget und die 512/64-MiB-Heapgrenzen bei. Die regulaere Suite
|
||||
prueft zusaetzlich begrenzte Zeichenarbeit, Originale und Browser-/Serverparitaet.
|
||||
|
||||
- HTML: `script`, `style`, eingebettete Frames/Objekte, Formulare, Medien, SVG und weitere aktive Elemente werden entfernt. Ereignisattribute und navigierende oder nachladende Attribute wie `src`, `srcdoc` und `action` werden vor der Tabellenauswertung gelöscht; `href` bleibt nur fuer sichere HTTP(S)-Beleglinks erhalten. Importierter Code wird nicht ausgeführt. Berichte oberhalb von 1 Mi Zeichen sowie DOM-Stacküberläufe wechseln automatisch in eine begrenzte, nicht rekursive Tabellenextraktion.
|
||||
- XML: `DOCTYPE`, `ENTITY`, `SYSTEM` und `PUBLIC` werden vor dem DOM-Parser abgewiesen. Externe Entitäten und lokale Dateireferenzen werden dadurch nicht aufgelöst.
|
||||
- Excel: Verschlüsselte ZIP-Einträge, nicht zugelassene Kompressionsverfahren, ausbrechende Archivpfade, ZIP64 und eingebettete `vbaProject.bin`-Inhalte werden abgewiesen. SheetJS läuft ohne Formel- oder VBA-Auswertung.
|
||||
- CSV-Export: Intern bleiben Originalwerte unverändert. Spreadsheet-orientierte Downloads, Aktionslisten und kopierte Tabellen neutralisieren führende `=`, `+`, `-` oder `@`, damit ein späteres Öffnen keine Formel aktiviert.
|
||||
- ZIP: Eintragsanzahl, Einzel- und Gesamtgröße sowie Kompressionsverhältnis werden ausschließlich aus dem Zentralverzeichnis vor dem Entpacken geprüft.
|
||||
|
||||
## PurpleKnight-Vollständigkeit
|
||||
|
||||
- Native HTML-Zuweisungen werden über den begrenzten statischen Datenleser
|
||||
verarbeitet, nicht evaluiert: maximal 128 Verschachtelungsebenen,
|
||||
1.000.000 Werte und 16 Mi Zeichen je Datenobjekt, 1.000.000 Zeichen je String
|
||||
sowie 250.000 Indikatoren je Bericht. Globale Datei-/Workergrenzen gelten zusätzlich.
|
||||
- Beschädigte Strukturen und fehlende native IDs scheitern explizit; ein
|
||||
angekündigter Ergebnisbestand darf nicht erfolgreich auf null reduziert werden.
|
||||
Mehrfache Zuweisung derselben Berichtswurzel oder desselben Datenobjekts ist
|
||||
ein Fehler. Wiederholte Regel-IDs erhalten eine Warnung und bleiben als
|
||||
getrennte Vorkommen erhalten.
|
||||
- Deklarierte Summen, Statuszahlen, Kategorieverweise und Detailanzahlen werden
|
||||
gegen die gelesenen Einzeleinträge geprüft. Widersprüche markieren den Import
|
||||
als partiell; Originalzähler und vollständige Einträge bleiben unverändert.
|
||||
- Explizite Null-Details sind bei null Ergebnissen gültig. Zeitstempel werden
|
||||
nicht zur Nachahmung eines Excel-Anzeigeformats auf Minuten gekürzt.
|
||||
- Workbook-Summary, Indikatorentabelle und Ergebnisblätter werden gemeinsam
|
||||
geprüft. CSV-Dateiname allein reicht weiterhin nicht zur Werkzeugfreigabe.
|
||||
|
||||
## PingCastle-Strukturtreue
|
||||
|
||||
Ab Profil 1.5.0 / Extractor 2.2.0 bewahrt der XML-Kontext alle Attribute als
|
||||
dekodierte Originalwerte mit `/@Name`-Quellpfad und ausdrücklich leere Blätter
|
||||
als `empty`. Namespace, Nullwert, `0`, `false` und leere Zeichenkette werden
|
||||
nicht gleichgesetzt. Die vorhandene Attribut-Anzeigezeichenkette bleibt erhalten;
|
||||
ältere Adapter ohne strukturierte Attribute erhalten eine Diagnose statt einer
|
||||
mehrdeutigen Rückkonvertierung. Der originale Dateitext bleibt unverändert.
|
||||
|
||||
HTML-Formatadapter 1.1.1 und XML-Formatadapter 1.1.0 liefern diese Struktur beziehungsweise eine
|
||||
attributgrenzenbewusste HTML-Extraktion. Spitze Klammern in zitierten Attributen
|
||||
werden nur für die inerte lexikalische Auswertung geschützt. Skripte werden
|
||||
nicht ausgewertet. Attributbereinigung arbeitet auf vollständigen Attributen,
|
||||
nicht auf scheinbaren `src`-/`href`-Angaben innerhalb eines Tooltips. Ein
|
||||
abgeschnittener End-Tag in einem begrenzten Textausschnitt wird nicht als
|
||||
Zellinhalt übernommen; ein nicht abgeschlossenes zitiertes Attribut im
|
||||
vollständigen Dokument führt zu einer Diagnose.
|
||||
|
||||
Fehlende native Rule IDs werden abgewiesen, doppelte IDs mit einer Warnung
|
||||
gemeldet. Die bestehende erste Darstellung bleibt das Finding. HTML-Trefferzahlen
|
||||
in den Kategorieüberschriften werden gegen eindeutige Regeln abgeglichen;
|
||||
Kataloggrößen sind keine Ergebniszahlen. Abweichungen oder ungültige Zähler
|
||||
markieren die Verarbeitung als `partial`, ohne gültige Regeln zu entfernen.
|
||||
Angekündigte Treffer ohne lesbare Ergebnisse scheitern; ausdrücklich deklarierte
|
||||
Nulltreffer bleiben zulässig. Originalzähler stehen in einem eigenen
|
||||
Berichtsabschnitt, nicht in der Assetzählung. Fehlende Zähler erlauben keinen
|
||||
belegten Vollständigkeitsanspruch. XML-Semantikmapping und Control-Path-JSON
|
||||
bleiben separate Arbeitspakete. Bestehende Importe werden nicht geändert.
|
||||
|
||||
## Statische Maester-Berichtsdaten
|
||||
|
||||
Der Maester-HTML-Extractor 1.1.0 unterstützt JSON und statische Objektliterale in
|
||||
Script-Zuweisungen sowie JSON-Datenblöcken. Der Locator überspringt Zeichenketten,
|
||||
Kommentare und übliche Regex-Literale; er ist kein allgemeiner JavaScript-Parser.
|
||||
Der anschließende Datenparser erlaubt keine Codeausführung und keine dynamischen
|
||||
Ausdrücke. Insbesondere werden Template-Interpolation, Aufrufe, Getter, Spread,
|
||||
berechnete und doppelte Schlüssel abgewiesen. Unbekannte statische Datenfelder
|
||||
bleiben mit ihrem Objektpfad erhalten.
|
||||
|
||||
Pro Datenobjekt gelten 16 Mi Zeichen, 128 Verschachtelungsebenen, eine Million
|
||||
Werte und eine Million Zeichen pro Zeichenkette. Pro Datei werden höchstens 32
|
||||
potenzielle Berichtsobjekte geprüft und 250.000 Tests akzeptiert. Eine
|
||||
Grenzverletzung oder ein beschädigter Test führt zum Fehler, nicht zur stillen
|
||||
Kürzung. Mehrere konkurrierende Berichtswurzeln werden abgewiesen; ein expliziter
|
||||
`Tenants`-Container erhält seine getrennten Mandantenpfade. Deklarierte Summen
|
||||
werden gegen Einzeltests geprüft. Abweichungen bleiben Warnungen mit beiden
|
||||
Zählwerten, ohne die Quelle oder die Ergebnismenge zu verändern.
|
||||
|
||||
## Vorschau, Persistenz und Protokollierung
|
||||
|
||||
Nicht bestätigte Vorschauen liegen höchstens 15 Minuten im Arbeitsspeicher. Der Cache ist auf 10 Batches und 256 MiB Quelldaten begrenzt. Abgelaufene oder verdrängte Vorschauen können nicht persistiert werden. Beim Abschluss werden IDs, SHA-256, Dateiname, Größe und Änderungszeit gegen die serverseitige Vorschau geprüft.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
# Reimport policy
|
||||
|
||||
ASSESSOR treats a reimport as a new, auditable interpretation of the unchanged original scanner source. The previous scan run remains stored and is linked to its replacement. Scanner-derived records may change; client-owned work must survive.
|
||||
|
||||
## When a reimport is required
|
||||
|
||||
A processing-profile version must be added and existing affected imports must be marked for reimport whenever a change can alter one or more of these persisted facts:
|
||||
|
||||
- scanner or report recognition;
|
||||
- parser or extractor coverage, including newly readable sections, native scores, counters, details, evidence, attributes, or context;
|
||||
- rule identity, finding boundaries, affected-asset identity, or duplicate handling;
|
||||
- field-role mapping, status, outcome, severity, IOE, normalization, or canonical projection;
|
||||
- report completeness diagnostics or security fixes that change which source records are accepted;
|
||||
- scoring inputs that were not retained by the earlier import.
|
||||
|
||||
Presentation-only changes, labels, layout, filtering over already retained fields, and score-formula changes over already retained inputs do not require a source reimport. They must not be disguised as processing changes.
|
||||
|
||||
Every scanner profile, including future profiles, declares its current processing contract. Historical contracts remain append-only. A material import change increments the relevant parser, extractor, profile, mapping, normalization, or presentation version and records `reimportRequired` plus the affected dimensions in `PROCESSING_VERSION_MANIFESTS`. Data Hub derives its warning from that persisted contract. A generic fallback remains visibly review-required until a scanner-specific profile can interpret the source.
|
||||
|
||||
## Preservation contract
|
||||
|
||||
Reimport replaces scanner-derived data only. The following client-owned values are preserved as one object and transferred to the replacement finding when the identity is unique:
|
||||
|
||||
- workflow status and custom label;
|
||||
- priority and action-plan inclusion;
|
||||
- owner and due date;
|
||||
- comments, prerequisites, required access, next step, and acceptance evidence;
|
||||
- markers, including accepted-risk state;
|
||||
- future additional review fields carried in the same record.
|
||||
|
||||
The match uses scanner identity, native rule ID, and affected asset. It succeeds only for one old and one new occurrence. A stable definition-level review remains attached without copying. ASSESSOR never falls back to title-only or positional matching.
|
||||
|
||||
When a match is absent or ambiguous, the old review remains on the retained previous scan. The replacement records an unresolved count and Data Hub shows a persistent review-required state. The reimport is still auditable and does not silently discard or guess customer data.
|
||||
|
||||
Both direct reimport and adoption of an already imported byte-identical replacement execute review transfer and scan supersession in the same repository transaction. The exact source SHA-256 multiset, assessment, and scanner must match before replacement is accepted.
|
||||
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
# ASSESSOR scoring rule 1.0.0
|
||||
|
||||
Decision: 2026-09-10, explicitly approved by the user. Implementation belongs to
|
||||
Phase 7/13C (Client Info and scanner context), with score-comparison safeguards
|
||||
under Phase 8. This decision supersedes the display-only native-score policy.
|
||||
|
||||
## Scan contribution
|
||||
|
||||
Use a valid native score from the exact import file and scan run. Otherwise use
|
||||
`100 * passed / (passed + deviations)` when the result population supports a
|
||||
pass rate. Never combine the native score and the pass rate of the same scan.
|
||||
|
||||
| Source | Native contribution | Fallback |
|
||||
| --- | --- | --- |
|
||||
| Purple Knight AD | Native AD posture, 0–100 | Pass rate |
|
||||
| CIS-CAT | Native benchmark score, 0–100 | Pass rate |
|
||||
| PingCastle Healthcheck | `100 - native risk`, bounded 0–100 | No pass rate from findings-only reports |
|
||||
| Maester | No native numeric score assumed | Pass rate |
|
||||
| GPO Analysis | None | Existing context-aware pass rate |
|
||||
| Additional registered scanners | Native overall score declared by their profile | Declared result-population fallback |
|
||||
| Unrecognized tabular sources | No native score inferred | Provisional pass rate only with evidence of passed results |
|
||||
|
||||
The Purple Knight AD score must not stand for a hybrid report containing other
|
||||
native posture scopes. Such reports use the disclosed fallback until supported
|
||||
scope-specific contributions are available. A missing native score on an old
|
||||
import requires the relevant context to be imported; no score is fabricated.
|
||||
|
||||
Null, blank, boolean, nonnumeric, out-of-range and conflicting native values
|
||||
remain unavailable. A valid zero is a real score. Invalid native data and source
|
||||
integrity diagnostics are visible in the calculation details.
|
||||
|
||||
Passed and deviations use normalized outcomes. Not applicable is excluded.
|
||||
Execution errors, not run and unknown results do not pass and do not add a
|
||||
security failure. They affect known-check coverage. GPO context states retain
|
||||
their existing authoritative disposition; no severity or missing-setting
|
||||
penalty is invented. Findings-only input cannot establish a pass denominator.
|
||||
|
||||
## Future scanner support
|
||||
|
||||
Every new or extended scanner profile must first examine the supported report
|
||||
formats for a usable native overall score. `ToolProfileDefinition.securityScoring`
|
||||
is mandatory: declare exact mapped observation properties, overall scope,
|
||||
0–100 scale, direction, incompatible scopes and fallback population, or record
|
||||
an explicit reason why native scoring is unavailable. A new profile registered
|
||||
in the profile catalog automatically uses the shared native-first calculation;
|
||||
there is no separate hardcoded scanner list in that calculation.
|
||||
|
||||
The extractor must retain the original score and its exact source path as
|
||||
file/run-bound report context. Different native scales or ambiguous scope need
|
||||
an explicitly documented and tested mapping before they are usable. Arbitrary
|
||||
fields named score, category scores and finding weights do not qualify. Until
|
||||
an unfamiliar format has such a profile, its native semantics remain unknown.
|
||||
|
||||
A valid native score, including zero, takes priority even for findings-only
|
||||
reports. Missing, invalid, conflicting or unrelated native observations lead
|
||||
to the declared fallback; without a defensible result population the contribution
|
||||
remains unavailable. Scoring-definition versions participate in comparison
|
||||
compatibility. Extend synthetic format fixtures and verify native priority,
|
||||
provenance, invalid values and fallback whenever adding scanner support.
|
||||
|
||||
This extension moves existing declarations into their profiles without changing
|
||||
their numeric methods or import mapping versions. No historic imports are
|
||||
rewritten. New extraction or semantic changes still require explicit versioning
|
||||
and a reimport recommendation for older data lacking the required context.
|
||||
|
||||
## Source weighting and provenance
|
||||
|
||||
Each source tool has one fixed weight, initially 1. Distinct scan scopes are
|
||||
averaged equally inside that weight; the overall value is the weighted mean of
|
||||
scoreable source contributions. File count and check count do not increase a
|
||||
tool's overall weight. Contributions with no score remain explicitly visible.
|
||||
Incomplete contributions make the displayed index provisional.
|
||||
|
||||
Same-run representations are combined only with a verified tool, assessed
|
||||
target, benchmark scope, exact reported timestamp and processing identity, or
|
||||
an identical SHA-256 and processing identity. Preserve the contributing file,
|
||||
run and occurrence references. Conflicting native scores or result populations
|
||||
make the combined contribution unscoreable. Source rule/outcome signatures
|
||||
also detect different results with coincidentally equal totals.
|
||||
|
||||
When a scanner exports one native score-only summary beside its finding file,
|
||||
the shared scan-run and processing identity may join those complementary files.
|
||||
The native score remains the contribution and the finding file supplies the
|
||||
passed/deviation population. This association requires exactly one native
|
||||
carrier and one unambiguous result population; ambiguous result files remain
|
||||
separate instead of borrowing the native value.
|
||||
|
||||
Within a known target and benchmark, use the latest reported scan time in the
|
||||
assessment. Never use import time or a filename to infer scan identity. Unknown
|
||||
identity/time remains disclosed. Original values and timestamps are unchanged.
|
||||
|
||||
The `Assessment.scoringPolicy` stores rule version, tool weights and default
|
||||
weight through the existing JSON/JSONB assessment contract. New scanner import
|
||||
assessments and new Data Hub timeslots initialize this field. Missing policies
|
||||
on historical assessments permanently mean the version 1.0.0 default, not an
|
||||
alias to a future current policy. Existing records are not migrated or rewritten.
|
||||
Unsupported policy versions do not produce a score. Round only at display time.
|
||||
|
||||
## Presentation and comparisons
|
||||
|
||||
Overall Score is a mixed-method assessment index, not a percentage of security.
|
||||
Native risk models and pass rates are not methodologically equivalent or
|
||||
empirically calibrated. Security Domain values are explicitly labelled pass
|
||||
rates and are not presented as a numeric decomposition of this index.
|
||||
|
||||
Source Overview shows the actual contribution and its method. The Scoring
|
||||
methodology dialog shows per-scan contribution, native input, source weight,
|
||||
evaluated-check basis, reported scan time and any qualifications. Critical
|
||||
findings and check coverage remain separate from the index.
|
||||
|
||||
Measured score progress uses only the one-to-one intersection of identical
|
||||
assessed targets/benchmarks with unchanged rule/evaluation population, processing, engine, method and
|
||||
weight, a newer source observation, and unqualified evidence. Both values are
|
||||
recomputed on this shared basis; omitting a weak source cannot become measured
|
||||
improvement. Changed scope/method is visibly labelled on the current index.
|
||||
|
||||
The workspace projects the latest known prior evidence per source scope from
|
||||
persisted historical assessments. Missing-source open obligations retain their
|
||||
original occurrence, run, assessment and observation-time references across
|
||||
successive absent follow-ups. Historical scores are never copied into current
|
||||
scan measurements. Unavailable history blocks a verified-progress statement
|
||||
and is visible without preventing access to the current workspace.
|
||||
|
||||
This is a read-only score/provenance projection. The broader Phase-8 workflow
|
||||
for persisted carry-forward lifecycle states, operational backlog integration
|
||||
and audited source retirement remains a separate unfinished work package.
|
||||
No new source retirement is inferred from a selection/filter operation.
|
||||
|
||||
## Manufacturer references
|
||||
|
||||
- [Semperis scoring methodology](https://www.semperis.com/blog/pk-scoring-improves-understanding-of-identity-system-security-vulnerabilities/): native posture and category scores have independent calculations.
|
||||
- [PingCastle Healthcheck](https://www.pingcastle.com/documentation/healthcheck/): overall risk uses the maximum of four capped category values. Inversion is an explicit ASSESSOR index convention.
|
||||
- [CIS-CAT report documentation](https://ciscat-assessor.docs.cisecurity.org/en/latest/ReportHTML/): scores are recommendation-based; scored recommendations currently have equal weight.
|
||||
|
||||
## Remaining limitations
|
||||
|
||||
- No new environment-area weighting or automatic mapping of native totals to
|
||||
the eight regex-assigned Security Domains.
|
||||
- Unknown source identity or engine/processing changes limit time comparisons.
|
||||
- Missing-result diagnostics qualify native scores rather than silently
|
||||
replacing source data. A source's scoring conventions can differ from check
|
||||
coverage conventions, especially for execution errors.
|
||||
- GPO-only history remains under the existing policy-comparison workflow.
|
||||
- This change does not reimport, migrate or inspect real assessment contents.
|
||||
|
||||
## Verification, 2026-09-10
|
||||
|
||||
`npm test`: 1,188 passed, eight optional PostgreSQL integration tests skipped;
|
||||
local-only security and design checks passed. Final focused run: 55 passed,
|
||||
including unchanged rule-population comparisons and persisted GPO weights.
|
||||
Final `npm run build`: client and server successful; existing bundle-size
|
||||
advisory remains. All content-level checks used synthetic fixtures. No real
|
||||
database changes, live content inspection, reimport or backend restart.
|
||||
|
||||
Follow-up for future scanner profiles: ten additional synthetic regressions
|
||||
cover the mandatory profile contract, native priority for a new scanner through
|
||||
dataset projection, score direction and zero, invalid/conflicting/foreign
|
||||
observations, unknown semantics, findings-only fallback and scoring versions.
|
||||
`npm test`: 1,198 passed, eight optional PostgreSQL tests skipped; local-only
|
||||
and design checks passed. `npm run build`: client and server successful.
|
||||
|
||||
Purple Knight companion-file follow-up: the score-only assessment summary is
|
||||
now associated with one unambiguous findings population from the exact same
|
||||
scan run and processing contract. The regression with 65 native posture,
|
||||
96 passed and 19 deviations yields 65 as the dashboard contribution while
|
||||
retaining all 19 deviations. `npm test`: 1,199 passed, eight optional
|
||||
PostgreSQL tests skipped; security/design checks and both builds passed.
|
||||
|
|
@ -54,7 +54,7 @@ Ein Parserworker begrenzt Zeit und Speicher, isoliert aber keinen bösartigen na
|
|||
| Manipulierte oder herstellerspezifisch erweiterte `Registry.pol`-Datensaetze erzeugen falsche Instruktionen oder Ressourcenverbrauch. | PReg-Signatur und Version, Feldabschluesse, Schluessel und Wertnamen, Typen, Datengroessen, Datensatzanzahl und Gesamtgroesse vor Reduktion begrenzen; nur freigegebene Typen und REG_NONE als exakt leere Schluesselanlage akzeptieren, sonst das Artefakt ohne Salvage blockieren. Keine Datei ausfuehren und keinen externen Prozess starten. |
|
||||
| GPP-`cpassword` oder andere Geheimnisse | Nur Vorhandensein und Locator erfassen; Wert bereits an der Parsergrenze maskieren, niemals entschlüsseln, loggen oder exportieren. |
|
||||
| Manipulierter oder unbeabsichtigt ausgetauschter Policy-Regelkatalog | Einen extern kuratierten Katalog nur aus einer lokalen regulären Nicht-Symlink-Datei bis 5 MiB laden, wenn Pfad und erwarteter SHA-256 gemeinsam konfiguriert sind. Hash vor UTF-8-/JSON-Parsing prüfen, danach den vollständigen fachlichen Katalogvertrag fail-closed validieren und bei jeder Abweichung vor Repository- und Serverstart abbrechen. Keine automatische Netzwerkquelle oder stille Aktivierung. |
|
||||
| Pfad-/Namensheuristik erzeugt falsche Policy- oder Zielidentität | Technische IDs ausschließlich aus unterstützten Artefaktinhalten und versionierten Regeln; Namen und Pfade bleiben Provenienz. |
|
||||
| Pfad-/Namensheuristik erzeugt falsche Policy- oder Zielidentität | Technische IDs ausschließlich aus unterstützten Artefaktinhalten und versionierten Regeln; Namen und Pfade bleiben Provenienz. Microsoft-Vorlagennamen werden nur auf ausdruecklichen Analyzer-Opt-in aus begrenzten XML-Feldern gelesen, nie als GPO-/Backup-/Policy-ID. Eine enge, versionierte Namensgrammatik darf den dokumentierten Vorlagenkontext beschreiben, aber nicht die Anwendung einer Kunden-GPO beweisen. Auswahl nur gegen genau einen expliziten Vergleichskontext; Inventarbeobachtungen, unbekannte Vorlagen und widerspruechliche Namen erzwingen keine Rollenwahl. |
|
||||
| Historische Backups werden gemeinsam aggregiert | Explizite Backup-GUID, GPO-GUID, Domänenidentität, manifestgestützte Set-ID und Erhebungsfenster prüfen; doppelte GPOs, Zeitkonflikte und Domänenmischung blockieren. |
|
||||
| Ausgewählte Teilmenge wird als vollständige Umgebung bewertet | Setvollständigkeit getrennt von Instanzvollständigkeit modellieren; negative Schlussfolgerungen nur bei manifestgestütztem vollständigem Set mit erwarteter und beobachteter Anzahl. |
|
||||
| Backup wird mit effektiver Anwendung verwechselt | Ohne RSoP-/Group-Policy-Results-Evidenz niemals Scope oder Wirksamkeit bestätigen; Ergebnisse als konfigurierte, nicht als effektiv angewandte Abweichung qualifizieren. |
|
||||
|
|
@ -92,6 +92,7 @@ Ein unsicherer Pfad, eine Kollision, ein Link, eine Grenzverletzung, ein Hashfeh
|
|||
- Links, Spezialdateien, verschachtelte ZIPs, verschlüsselte oder mehrteilige Archive, ZIP64 sowie nicht eindeutig UTF-8-kodierte Nicht-ASCII-Pfade blockieren das gesamte Inventar.
|
||||
- Der GPO-Metadatenparser läuft im begrenzten Worker, akzeptiert nur begrenztes UTF-8 beziehungsweise BOM-markiertes UTF-16, lehnt DTD, Entitäten, externe Referenzen, XInclude, Schemas und fremde Processing Instructions ab und übernimmt technische IDs nur konfliktfrei aus Inhalt. `cpassword` wird nur als Indikator erkannt; der Wert verlässt die Parsergrenze nicht.
|
||||
- Der Security-Template-Parser läuft in derselben Ressourcenbegrenzung und erzwingt Datei-, Zeichen-, Zeilen- und INF-Feldgrenzen. Unterstützte Abschnitte werden semantisch getrennt reduziert; unterschiedliche Dubletten bleiben Konflikte, unbekannte Abschnitte `partial`, ungültige Registry-Instruktionen blockiert und effektive Anwendung bleibt ohne RSoP immer `unknown`. Rohwerte werden nicht über die bestehende Backend-/Projektgrenze veröffentlicht.
|
||||
- Präzisierung 2026-09-08 (Security-Template-Parser 1.1.0): Metadaten und unbekannte Abschnitte werden vor der Key/Value-Validierung übersprungen, da beispielsweise Service-Einträge eine eigene CSV-artige Grammatik haben. Isolierte defekte Einträge in unterstützten Abschnitten werden mit Diagnose verworfen; gültige Instruktionen behalten Originalwert und Quellzeile bei Dokumentstatus `partial`. Dies ersetzt ausschließlich die bisherige artefaktweite Verwerfung wegen einzelner defekter INF-Einträge. Beschädigte Abschnittsgrenzen, Einträge ohne Abschnitt und Ressourcenverletzungen bleiben blockiert; Dekodierungsfehler werden weiterhin abgewiesen. `partial`/`inventory-only` bleiben in Baselineanalyse und Kundeninventar sichtbar und erlauben keine Vollständigkeitsbehauptung. Historische Analysen bleiben erhalten; Baseline-Analyzer 1.2.0 und GPO-Processing 2.2.3 erzeugen versionierte Nachfolger.
|
||||
- Der Advanced-Audit-Parser verwendet den allgemeinen CSV-Pfad nur zur begrenzten lexikalischen Dekodierung mit physischen Quellzeilen. Er verlangt die technischen Spalten `Policy Target`, `Subcategory GUID` und `Setting Value`, akzeptiert nur kanonische Subcategory-GUIDs sowie numerische Flags 0 bis 3 und ignoriert lokalisierte Anzeigenamen. Nur Systemziele werden reduziert; benutzerspezifische Ziele bleiben `partial`. Abweichende GUID-Dubletten bleiben Konflikte, ungültige unterstützte Zeilen blockieren ohne Salvage und `effectiveState` bleibt ohne RSoP `unknown`. Advanced und Legacy Audit werden nicht zusammengeführt.
|
||||
- Der `Registry.pol`-Parser laeuft im begrenzten GPO-Worker und akzeptiert ausschliesslich PReg Version 1 mit festen Datei-, Datensatz- und Datenfeldgrenzen. Er dekodiert freigegebene Registrytypen typisiert, erhaelt Instruktionsreihenfolge und Byteoffset-Provenienz und behandelt REG_NONE nur als leere Schluesselanlage. Delete-, DeleteValues-, DeleteKeys-, SecureKey- und Soft-Operationen bleiben getrennt; unbekannte Typen und fehlerhafte unterstuetzte Strukturen blockieren ohne Salvage, unbekannte reservierte Operationen bleiben als nicht unterstuetzt sichtbar. Weder Shell noch LGPO.exe noch Netzwerk werden verwendet.
|
||||
- Der Group-Policy-Preferences-Parser läuft in derselben Ressourcenbegrenzung, akzeptiert nur begrenztes UTF-8 beziehungsweise BOM-markiertes UTF-16 und lehnt DTD, Entitäten, externe Referenzen, Schemas sowie fremde Processing Instructions ab. Registry Preferences benötigen die dokumentierten technischen CLSIDs, kanonische Item-UIDs, gültige Hives, Ziele, Werttypen, Aktionen und boolesche Common Options. Verarbeitungsreihenfolge, Disabled, Remove-on-unapply und Apply-once bleiben erhalten. Item-Level-Targeting wird strukturell validiert, aber ohne Umgebungsevidenz nur durch Filtertypen und Ausdruckshash als nicht auswertbar gebunden. Unbekannte CSEs beziehungsweise Registry-SubProperties bleiben `partial`; fehlerhafte unterstützte Strukturen blockieren ohne Salvage. `cpassword` wird nur als Indikator erkannt, nie entschlüsselt oder in Dokument, Beobachtung, Diagnose beziehungsweise Export übernommen.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
# Navigation performance
|
||||
|
||||
This is the Phase 7 measurement and regression procedure for area switches.
|
||||
|
||||
## Local measurement
|
||||
|
||||
1. Start the existing local app and backend using the canonical PostgreSQL datastore. Do not create a performance-test database.
|
||||
2. Use synthetic fixtures or an explicitly authorized dummy workspace. Open the development app at `http://127.0.0.1:5173/?measureNavigation=1` and load that workspace. Ordinary app URLs do not enable instrumentation; production builds exclude it.
|
||||
3. Wait for the workspace to finish loading. Keep viewport, data, filter settings and development/production mode identical between comparisons. Stop concurrent builds and load tests before recording timings.
|
||||
4. Click each main navigation area. Read `#assessor-navigation-performance` after the next three animation frames, waiting for `sequence` to advance. It exposes only `area`, `sequence`, `paintMs`, `longestFrameMs` and named calculation durations in `stepsMs`. The interval starts in the click capture handler. This measures click-to-render responsiveness, not the completion of backend requests. Check asynchronous readiness separately using control availability and row counts. Initial data loading must not overlap a navigation measurement.
|
||||
5. Measure a first visit and at least two repeat visits. Test Work before and after opening Pathfinder, and test the return from Datahub and Result DB. Confirm that filter changes made while the graph is hidden are applied on reactivation and that a workspace change resets retained state.
|
||||
6. Close temporary browser tabs after testing. Never copy customer content into timing reports.
|
||||
|
||||
## Known causes and controls
|
||||
|
||||
| Cause found on 2026-09-09 | Control |
|
||||
| --- | --- |
|
||||
| Filter bar, sidebar and viewer independently repeated equivalent queries after remounts. | Shared bounded sheet selectors; reference/semantic keys include every input that changes the result. |
|
||||
| Status matching rescanned source fields for each consumer and facet. | Profile-owned weak record caches; keep the existing matching semantics and profile separation. |
|
||||
| Rejected numbered column groups reread all siblings for every column. | Reuse the group values, skip ineligible text columns early, stop eligibility checks beyond the category limit. |
|
||||
| Sort comparators normalized both records on every comparison. | Normalize once per record before sorting; preserve tie-breaking and null ordering. |
|
||||
| Work serialized every filtered finding into Raw Data text while displaying other modes. | Mount and memoize the complete text projection only in Raw Data. Derive titles only for rendered records. |
|
||||
| Virtualization initially mounted 40 rows before measuring the viewport. | Bound the first window by screen height plus overscan; use stable callback refs and reset cancelled frame handles. |
|
||||
| Work mounted 2,537 native sheet options on every visit to the authorized dummy. | Keep the selected option mounted; synchronously prepare all options before pointer opening or keyboard focus. Collapse on blur without changing selection. |
|
||||
| Export buttons scanned the workspace and constructed export rows during rendering. | Determine availability with a short-circuit check; construct the payload on the export action. |
|
||||
| Hidden Pathfinder built its graph, consumed context updates and measured hidden geometry. | First-activation mount, retained input boundary, scope reset, memoized component, paused hidden observers/animations. |
|
||||
| Graph text collision layout ran with text switched off; identical MITRE headers were repeatedly classified. | Gate text layout by display mode and reuse header classifications within each graph derivation. |
|
||||
| An unchanged periodic health response replaced the global app state. | Preserve state identity unless the health status changes. |
|
||||
| Repeated identical geometry observations replaced graph/dashboard state. | Compare dimensions and connector paths before setting state. |
|
||||
|
||||
## Regression checks
|
||||
|
||||
`tests/features/navigation-performance.test.ts` exercises 8,257 synthetic records with repeated numbered columns. The unchanged repeat visit must read zero source fields. Smaller cases verify review, status, severity, search, path, profile, dataset and sheet invalidation, source preservation and bounded cache eviction.
|
||||
|
||||
`tests/features/navigation-rendering.test.tsx` verifies retained-view suspension/resumption, raw-text reuse and the first virtual window. `tests/features/dataset-viewer-cost.test.tsx` mounts the actual viewer: Matrix must not read an offscreen finding's fields, while Raw Data must include its complete text. `tests/features/sheet-selection-cost.test.tsx` verifies 2,537 synthetic choices, pointer/keyboard preparation and preservation of the selected sheet after blur.
|
||||
|
||||
Run the focused tests while iterating, then `npm test` and `npm run build`. Existing facet, severity, GPO, result-window and graph tests protect the underlying behavior. Timing output is diagnostic; CI uses deterministic behavior and operation counts rather than machine-dependent millisecond limits.
|
||||
|
||||
## Observations on 2026-09-09
|
||||
|
||||
Development browser, authorized dummy, 1280 × 720 viewport: the initial observed Work switch took 5,324 ms. After shared calculations, lazy raw text, bounded first rendering and deferred sheet options, unchanged Work revisits measured 121–211 ms, including two consecutive measurements of 136/138 ms. One return immediately after a filter reset and graph rebuild took 406 ms. These are individual observations, not a hardware-independent latency guarantee.
|
||||
|
||||
The final cross-area sweep measured 34 ms for Result DB, 44 ms for DB Structure and 109 ms for Datahub until the third frame; their asynchronous data requests are not included. Client Info measured 353 ms on a warm visit, while the first visit after filter reset took 991 ms. Pathfinder still performs a necessary first build or rebuild after data/filter changes (1,005 ms after the reset in this sweep); retained unchanged visits avoid rebuilding its data. Continue measuring these separate cold costs when changing either area.
|
||||
|
||||
The synthetic 8,257-record benchmark initially measured 5,360 ms cold / 398 ms repeated. Focused post-change runs measured 623–978 ms cold / 1 ms repeated; concurrent full-suite load raised cold times, so those runs are not directly comparable. The reliable regression invariant is zero source-field reads on an unchanged repeat visit. No tenant data, source artifacts or database schema were changed for these measurements.
|
||||
|
|
@ -16,6 +16,89 @@
|
|||
</table>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
Kopfangaben, Bewertungsanzeige und Abschnittsuebersicht eines
|
||||
CIS-CAT-Pro-Berichts.
|
||||
|
||||
Die Zahlen des Summary-Blocks entsprechen genau den beiden
|
||||
Ergebniszeilen dieser Datei (1 Pass, 1 Fail), damit die bestehende
|
||||
Gegenprobe des Imports unveraendert aufgeht. Die Spaltennamen der
|
||||
Abschnittsuebersicht sind woertlich uebernommen ("Unkn.", "Man.",
|
||||
"Exc."), weil der Import genau an ihnen erkennt, welche Tabelle den
|
||||
Punktwert je Benchmarkabschnitt traegt. Die Systemangaben stehen als
|
||||
beschriftete Wertepaare, nicht an einer festen Stelle im Aufbau.
|
||||
-->
|
||||
<!--
|
||||
Titelseite eines CIS-CAT-Pro-Berichts, im Aufbau uebernommen: das
|
||||
bewertete System steht als "for <Host>" in der zweiten Ueberschrift, die
|
||||
Netzadresse als "Target IP Address" im Listenpunkt darunter, und Profil,
|
||||
Zeitpunkt sowie Dauer stehen als unbeschriftete Listenpunkte unter der
|
||||
Benchmarkzeile. Beschriftete Wertepaare gibt es hier nicht.
|
||||
-->
|
||||
<div id="coverPageTitle">
|
||||
<h1>Security Configuration Assessment Report</h1>
|
||||
<h1>for SYNTHETIC-HOST-01</h1>
|
||||
<ul>
|
||||
<li>Target IP Address: 203.0.113.10</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="coverPageSubTitle">
|
||||
<h2>Synthetic CIS Benchmark v2.0.0</h2>
|
||||
<ul>
|
||||
<li>Level 1 - Synthetic Member Server</li>
|
||||
<li>Friday, July 17 2026 08:00:00</li>
|
||||
<li> Assessment Duration: 1 minutes, 5 seconds</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="introFooter">
|
||||
<p>Report generated by the Center for Internet Security's Configuration Assessment Tool (CIS-CAT Pro Assessor) v4.61.0.</p>
|
||||
</div>
|
||||
|
||||
<section id="summary-metrics">
|
||||
<table class="summary-full">
|
||||
<tr>
|
||||
<th colspan="6">
|
||||
<div class="metric-value">1<span class="total-metric-value"> / 2</span></div>
|
||||
<div class="summary-label">Passed Recommendations</div>
|
||||
</th>
|
||||
<td rowspan="2" class="summary-score"><div class="gauge" data-score="50"></div></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="metric-value pass">1</div><div class="metric-label">Pass</div></td>
|
||||
<td><div class="metric-value fail">1</div><div class="metric-label">Fail</div></td>
|
||||
<td><div class="metric-value error">0</div><div class="metric-label">Error</div></td>
|
||||
<td><div class="metric-value unknown">0</div><div class="metric-label">Unknown</div></td>
|
||||
<td><div class="metric-value manual">0</div><div class="metric-label">Manual</div></td>
|
||||
<td><div class="metric-value excepted">0</div><div class="metric-label">Excepted</div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="sectionScores">
|
||||
<table class="summary-sections">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th><th>Pass</th><th>Fail</th><th>Error</th><th>Unkn.</th>
|
||||
<th>Man.</th><th>Exc.</th><th>Score</th><th>Max</th><th>Percent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1 Account Policies</td><td>1</td><td>1</td><td>0</td><td>0</td>
|
||||
<td>0</td><td>0</td><td>1.0</td><td>2.0</td><td>50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1.1 Password Policy</td><td>1</td><td>0</td><td>0</td><td>0</td>
|
||||
<td>0</td><td>0</td><td>1.0</td><td>1.0</td><td>100%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2 Local Policies</td><td>0</td><td>0</td><td>0</td><td>0</td>
|
||||
<td>0</td><td>0</td><td>0.0</td><td>0.0</td><td>0%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section id="profiles">
|
||||
<h1>Profiles</h1>
|
||||
<table class="profile">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
<!doctype html><html><head><title>Maester Security Tests</title></head><body>
|
||||
<div id="root"></div><script>
|
||||
const unrelated = "This is not report data", testResults = {
|
||||
Result: "Failed", TotalCount: 5, FailedCount: 0, PassedCount: 1, InvestigateCount: 1,
|
||||
ErrorCount: 1, SkippedCount: 1, NotRunCount: 1,
|
||||
TenantId: "synthetic-tenant-id", TenantName: "Synthetic tenant",
|
||||
ExecutedAt: "2026-09-09T08:00:00Z", CurrentVersion: "1.0.0", TotalDuration: "00:00:10",
|
||||
SystemInfo: { OS: "Synthetic OS", Available: !0, Count: 0, Nullable: null },
|
||||
PowerShellInfo: { Version: "7.4" }, LoadedModules: [], FutureMetadata: { nested: ['retained'] },
|
||||
Tests: [
|
||||
{ Id: "MT.9901", Title: "Synthetic failed test", Result: "Failed", Severity: "High",
|
||||
Tag: ["Synthetic", "Identity"], Block: "Synthetic block", Duration: "00:00:01",
|
||||
ResultDetail: { TestDescription: "<p>Synthetic description</p>", TestResult: "<p>Synthetic evidence</p>",
|
||||
Severity: "High", FutureDetail: { retained: !0 }, TestSkipped: !1 },
|
||||
ScriptBlock: `Write-Output 'inert text'
|
||||
@{ Key = "value" }`, HelpUrl: "https://maester.dev/docs/tests/MT.9901", FutureField: 0 },
|
||||
{ Id: "MT.9902", Title: "Synthetic investigate test", Result: "Investigate", Severity: "Medium", ResultDetail: { TestResult: "Manual review" } },
|
||||
{ Id: "MT.9903", Title: "Synthetic passed test", Result: "Passed", Severity: "Low", ResultDetail: { TestResult: "Synthetic success" } },
|
||||
{ Id: "MT.9904", Title: "Synthetic skipped test", Result: "Skipped", Severity: "Info", ResultDetail: { TestSkipped: !0, SkippedReason: "Synthetic missing permission" } },
|
||||
{ Id: "MT.9905", Title: "Synthetic not-run test", Result: "NotRun", ResultDetail: { SkippedReason: "Synthetic disabled test" } },
|
||||
{ Id: "MT.9906", Title: "Synthetic error test", Result: "Error", ErrorRecord: [{ Message: "Synthetic execution error" }] },
|
||||
], Blocks: [{ Name: "Synthetic block" }], EndOfJson: "EndOfJson",
|
||||
};
|
||||
globalThis.__maesterActiveContentExecuted = true;
|
||||
</script></body></html>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Tool edition,Community
|
||||
Generated date,2026-01-15 09:30
|
||||
Selected indicators,120
|
||||
AD: scan duration,00:00:21
|
||||
AD: forest name,synthetic.example
|
||||
AD: number of domains,2
|
||||
AD: requested by,SYNTHETIC\Administrator
|
||||
AD: security posture score,71
|
||||
AD category score: AD Delegation,84
|
||||
AD category score: Account Security,79
|
||||
AD category score: AD Infrastructure Security,92
|
||||
AD category score: Group Policy Security,97
|
||||
AD category score: Kerberos Security,95
|
||||
AD category score: Hybrid,100
|
||||
AD: Evaluated,117
|
||||
AD: IOEs found,17
|
||||
AD: Passed,100
|
||||
AD: Failed to run,0
|
||||
AD: Canceled,0
|
||||
AD: Not selected,1
|
||||
AD: Not relevant,2
|
||||
|
Binary file not shown.
|
|
@ -0,0 +1,951 @@
|
|||
{"kind":"workbook","id":"wb/g5a6mv","sheets":12,"tables":12}
|
||||
{"kind":"sheet","id":"ws/e4sy1m","name":"September 2026","index":0,"range":"A1:AG5","address":"A1:AG5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"September 2026","address":"A1:AG5","rows":5,"cols":33,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,"Monatssumme"],["Bereich","ASSESSOR","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Std."],["Tätigkeit",null,null,null,"Client Info; GPO-Regeln und Analysepfad; Tests","GPO-Import robuster; nicht lesbare Einträge behandelt",null,"GPO-Auswertung testbereit; UI/Pfade; Regressionstests","Scan-Navigation; GPO-Duplikate und Ergebnisabgleich","GPO-Quellen konsolidiert; Historie und Tests","Import-Timeout; Navigation-Performance; Tests/Build",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,5,9.5,null,9,4,7,8,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,42.5],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,42.5]]}
|
||||
{"kind":"region","sheet":"September 2026","address":"A1:AG5","rows":5,"cols":33,"nonEmpty":84,"blank":81,"text":84,"maxTextLength":53,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Di","Mi","Do","Fr"],["Tätigkeit",null,null,null,"Client Info; GPO-Regeln und Analysepfad; Tests","GPO-Import robuster; nicht lesbare Einträge behandelt"],["Stunden",null,null,null,"5","9.5"],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"September 2026","address":"AG4","formula":"=SUM(C4:AF4)"}
|
||||
{"kind":"formula","sheet":"September 2026","address":"AG5","formula":"=AG4"}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"E3","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"left","styleId":62}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"F3","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"left","styleId":62}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"H3","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":65}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"I3","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"left","styleId":62}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"J3","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"left","styleId":62}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"K3","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"left","styleId":62}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AG3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"E4","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":67}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"F4","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":67}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"H4","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":true,"horizontalAlignment":"center","styleId":68}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"I4","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":67}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"J4","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":67}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"K4","style":{"fill":{"type":3,"color":{"type":1,"value":"F2F2F2"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":67}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AG4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":70}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"September 2026","for":"AG5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":71}}
|
||||
{"kind":"sheet","id":"ws/hwa6ib","name":"Oktober 2026","index":1,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Oktober 2026","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Oktober 2026","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Do","Fr","Sa","So"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Oktober 2026","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"Oktober 2026","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Oktober 2026","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/t14qa1","name":"November 2026","index":2,"range":"A1:AG5","address":"A1:AG5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"November 2026","address":"A1:AG5","rows":5,"cols":33,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,"Monatssumme"],["Bereich","ASSESSOR","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"November 2026","address":"A1:AG5","rows":5,"cols":33,"nonEmpty":72,"blank":93,"text":72,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","So","Mo","Di","Mi"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"November 2026","address":"AG4","formula":"=SUM(C4:AF4)"}
|
||||
{"kind":"formula","sheet":"November 2026","address":"AG5","formula":"=AG4"}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AG3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AG4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"November 2026","for":"AG5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/cyk7i5","name":"Dezember 2026","index":3,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Dezember 2026","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Dezember 2026","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Di","Mi","Do","Fr"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Dezember 2026","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"Dezember 2026","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Dezember 2026","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/pbjxg6","name":"Januar 2027","index":4,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Januar 2027","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Januar 2027","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Fr","Sa","So","Mo"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Januar 2027","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"Januar 2027","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Januar 2027","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/w1giyz","name":"Februar 2027","index":5,"range":"A1:AE5","address":"A1:AE5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Februar 2027","address":"A1:AE5","rows":5,"cols":31,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,"Monatssumme"],["Bereich","ASSESSOR","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Februar 2027","address":"A1:AE5","rows":5,"cols":31,"nonEmpty":68,"blank":87,"text":68,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Mo","Di","Mi","Do"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Februar 2027","address":"AE4","formula":"=SUM(C4:AD4)"}
|
||||
{"kind":"formula","sheet":"Februar 2027","address":"AE5","formula":"=AE4"}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AE3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AE4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Februar 2027","for":"AE5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/hidpcf","name":"März 2027","index":6,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"März 2027","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"März 2027","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Mo","Di","Mi","Do"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"März 2027","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"März 2027","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"März 2027","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/7bmf40","name":"April 2027","index":7,"range":"A1:AG5","address":"A1:AG5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"April 2027","address":"A1:AG5","rows":5,"cols":33,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,"Monatssumme"],["Bereich","ASSESSOR","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"April 2027","address":"A1:AG5","rows":5,"cols":33,"nonEmpty":72,"blank":93,"text":72,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Do","Fr","Sa","So"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"April 2027","address":"AG4","formula":"=SUM(C4:AF4)"}
|
||||
{"kind":"formula","sheet":"April 2027","address":"AG5","formula":"=AG4"}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AG3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AG4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"April 2027","for":"AG5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/9bfmxu","name":"Mai 2027","index":8,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Mai 2027","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Mai 2027","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Sa","So","Mo","Di"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Mai 2027","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"Mai 2027","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":23}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"C0504D"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":24}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Mai 2027","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/dnm34v","name":"Juni 2027","index":9,"range":"A1:AG5","address":"A1:AG5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Juni 2027","address":"A1:AG5","rows":5,"cols":33,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,"Monatssumme"],["Bereich","ASSESSOR","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Juni 2027","address":"A1:AG5","rows":5,"cols":33,"nonEmpty":72,"blank":93,"text":72,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Di","Mi","Do","Fr"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Juni 2027","address":"AG4","formula":"=SUM(C4:AF4)"}
|
||||
{"kind":"formula","sheet":"Juni 2027","address":"AG5","formula":"=AG4"}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":5}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":8}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AG3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AG4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Juni 2027","for":"AG5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/eilfeu","name":"Juli 2027","index":10,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"Juli 2027","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,0]]}
|
||||
{"kind":"region","sheet":"Juli 2027","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":74,"blank":96,"text":74,"maxTextLength":16,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Do","Fr","Sa","So"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"Juli 2027","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"Juli 2027","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"A1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"B1","style":{"fill":{"type":3,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":27}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"C1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":3}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"D1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"E1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"F1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"G1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"H1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"I1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"J1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"K1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"L1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"M1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"N1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"O1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"P1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Q1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"R1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"S1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"T1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"U1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"V1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"W1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"X1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Y1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Z1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AA1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AB1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AC1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AD1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AE1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AF1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":4}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AG1","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":19}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AH1","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"A2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"B2","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":1}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"C2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":6}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"D2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"E2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"F2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"G2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"H2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"I2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"J2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"K2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"L2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"M2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"N2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"O2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"P2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Q2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"R2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"S2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"T2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"U2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"V2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"W2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"X2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Y2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"Z2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AA2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AB2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AC2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AD2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AE2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AF2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":7}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AG2","style":{"fill":{"type":3,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":true,"horizontalAlignment":"center","styleId":20}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AH2","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{},"wrapText":false,"horizontalAlignment":"center","styleId":2}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"A3","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":28}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AH3","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":15}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"A4","style":{"fill":{"type":3,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":30}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AH4","style":{"fill":{"type":3,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":16}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"A5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"left":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":32}}
|
||||
{"kind":"computedStyle","sheet":"Juli 2027","for":"AH5","style":{"fill":{"type":3,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pattern":{"patternType":2},"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito"},"border":{"top":{"style":"thin","color":{"type":1,"value":"000000"}},"bottom":{"style":"thin","color":{"type":1,"value":"000000"}},"right":{"style":"thin","color":{"type":1,"value":"000000"}}},"wrapText":false,"horizontalAlignment":"center","styleId":18}}
|
||||
{"kind":"sheet","id":"ws/y1qvb8","name":"August 2026","index":11,"range":"A1:AH5","address":"A1:AH5","tables":1,"formulas":2}
|
||||
{"kind":"table","sheet":"August 2026","address":"A1:AH5","rows":5,"cols":34,"values":[["ASSESSOR","Zeiterfassung",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,"Monatssumme"],["Bereich","ASSESSOR","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Di","Mi","Do","Fr","Sa","So","Mo","Std."],["Tätigkeit",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,"Repo-Import; PingCastle-Mapping; Dashboard; PostgreSQL/CI",null,"UI-Anpassungen; Purple Knight/PingCastle HTML-Import",null,null,null,null,null,"Summe"],["Stunden",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,7,null,7.5,null,null,null,null,null,14.5],["Gesamtmonatsumme",null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,14.5]]}
|
||||
{"kind":"region","sheet":"August 2026","address":"A1:AH5","rows":5,"cols":34,"nonEmpty":78,"blank":92,"text":78,"maxTextLength":57,"preview":[["ASSESSOR","Zeiterfassung","1","2","3","4"],["Bereich","ASSESSOR","Sa","So","Mo","Di"],["Tätigkeit",null,null,null,null,null],["Stunden",null,null,null,null,null],["Gesamtmonatsumme",null,null,null,null,null]],"previewAddress":"A1:F5","previewRows":5,"previewCols":6}
|
||||
{"kind":"formula","sheet":"August 2026","address":"AH4","formula":"=SUM(C4:AG4)"}
|
||||
{"kind":"formula","sheet":"August 2026","address":"AH5","formula":"=AH4"}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"A1","style":{"fill":{"type":1,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":39}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"B1","style":{"fill":{"type":1,"color":{"type":1,"value":"00E640"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":12,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":39}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"C1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"D1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"E1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"F1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"G1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"H1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"I1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"J1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"K1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"L1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"M1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"N1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"O1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"P1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Q1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"R1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"S1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"T1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"U1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"V1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"W1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"X1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Y1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Z1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AA1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AB1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AC1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AD1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AE1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AF1","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AG1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AH1","style":{"fill":{"type":1,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":55}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"A2","style":{"fill":{"type":1,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":41}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"B2","style":{"fill":{"type":1,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":41}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"C2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"D2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"E2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"F2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"G2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"H2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"I2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"J2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"K2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"L2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"M2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"N2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"O2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"P2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Q2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"R2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"S2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"T2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"U2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"V2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"W2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"X2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Y2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Z2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AA2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AB2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AC2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AD2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AE2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AF2","style":{"fill":{"type":1,"color":{"type":1,"value":"2E65A3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":52}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AG2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFACD"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":45}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AH2","style":{"fill":{"type":1,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":55}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"A3","style":{"fill":{"type":1,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":43}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Z3","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":56}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AB3","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":true,"horizontalAlignment":"center","styleId":56}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AH3","style":{"fill":{"type":1,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":55}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"A4","style":{"fill":{"type":1,"color":{"type":1,"value":"E7E6E6"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":43}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"Z4","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":58}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AB4","style":{"fill":{"type":1,"color":{"type":1,"value":"FFFFFF"},"gradientStops":[],"pictureEffects":[]},"font":{"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":58}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AH4","style":{"fill":{"type":1,"color":{"type":1,"value":"FFF2CC"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":59}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"A5","style":{"fill":{"type":1,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"wrapText":false,"horizontalAlignment":"center","styleId":41}}
|
||||
{"kind":"computedStyle","sheet":"August 2026","for":"AH5","style":{"fill":{"type":1,"color":{"type":1,"value":"D9EAD3"},"gradientStops":[],"pictureEffects":[]},"font":{"bold":true,"fontSize":11,"typeface":"Carlito","fill":{"type":0,"color":{"type":1,"value":"000000"},"gradientStops":[],"pictureEffects":[]}},"border":{"top":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"bottom":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"left":{"style":"thin","color":{"type":1,"value":"B7B7B7"}},"right":{"style":"thin","color":{"type":1,"value":"B7B7B7"}}},"numberFormat":"0.0","wrapText":false,"horizontalAlignment":"center","styleId":60}}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const inputPath = "C:/Users/milu/OneDrive - Exxeta/Dokumente/Assessor Zeiterfassung/ASSESSOR_Zeiterfassung_Sep2026_Jul2027_Form_wie_vorher.xlsx";
|
||||
const outputDir = "C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/outputs/01a089e9-f688-7f21-87c9-db223e98841f";
|
||||
|
||||
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(inputPath));
|
||||
const overview = await workbook.inspect({
|
||||
kind: "workbook,sheet,table,definedName,drawing",
|
||||
maxChars: 12000,
|
||||
tableMaxRows: 8,
|
||||
tableMaxCols: 10,
|
||||
tableMaxCellChars: 100,
|
||||
});
|
||||
console.log(overview.ndjson);
|
||||
|
||||
const sheets = workbook.worksheets.items;
|
||||
for (const sheet of sheets) {
|
||||
const used = sheet.getUsedRange();
|
||||
console.log(`SHEET\t${sheet.name}\t${used?.address ?? "(empty)"}`);
|
||||
if (used) {
|
||||
const region = await workbook.inspect({
|
||||
kind: "region,formula,computedStyle",
|
||||
sheetId: sheet.name,
|
||||
range: used.address,
|
||||
maxChars: 10000,
|
||||
tableMaxRows: 45,
|
||||
tableMaxCols: 12,
|
||||
tableMaxCellChars: 120,
|
||||
options: { maxResults: 120 },
|
||||
});
|
||||
console.log(region.ndjson);
|
||||
const preview = await workbook.render({ sheetName: sheet.name, autoCrop: "all", scale: 1, format: "png" });
|
||||
const safe = sheet.name.replace(/[^a-z0-9_-]+/gi, "_");
|
||||
await fs.writeFile(`${outputDir}/preview_${safe}.png`, new Uint8Array(await preview.arrayBuffer()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
const wb = await SpreadsheetFile.importXlsx(await FileBlob.load("C:/Users/milu/OneDrive - Exxeta/Dokumente/Assessor Zeiterfassung/ASSESSOR_Zeiterfassung_Sep2026_Jul2027_Form_wie_vorher.xlsx"));
|
||||
for (const address of ["E1","F1","E2","F2","E3","F3","E4","F4"]) {
|
||||
const out = await wb.inspect({ kind: "computedStyle", sheetId: "Oktober 2026", range: address, maxChars: 1500 });
|
||||
console.log(address, out.ndjson);
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const inputPath = "C:/Users/milu/OneDrive - Exxeta/Dokumente/Assessor Zeiterfassung/ASSESSOR_Zeiterfassung_Sep2026_Jul2027_Form_wie_vorher.xlsx";
|
||||
const outputDir = "C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/outputs/01a089e9-f688-7f21-87c9-db223e98841f";
|
||||
const outputPath = `${outputDir}/ASSESSOR_Zeiterfassung_Aug2026_Jul2027_aktualisiert.xlsx`;
|
||||
|
||||
const workbook = await SpreadsheetFile.importXlsx(await FileBlob.load(inputPath));
|
||||
|
||||
// The supplied workbook starts in September. Add August using the existing
|
||||
// 31-day October sheet as the structural and formatting reference.
|
||||
const october = workbook.worksheets.getItem("Oktober 2026");
|
||||
const august = workbook.worksheets.add("August 2026");
|
||||
august.getRange("A1:AH5").copyFrom(october.getRange("A1:AH5"), "all");
|
||||
|
||||
// Normalize all date columns to the existing weekday style, then apply the
|
||||
// existing Saturday/Sunday pair style to the five August weekends.
|
||||
for (let col = 2; col <= 32; col += 1) {
|
||||
august.getRangeByIndexes(0, col, 4, 1).copyFrom(october.getRange("C1:C4"), "all");
|
||||
}
|
||||
for (const startCol of [2, 9, 16, 23, 30]) {
|
||||
august.getRangeByIndexes(0, startCol, 4, 2).copyFrom(october.getRange("E1:F4"), "all");
|
||||
}
|
||||
|
||||
const weekdayNames = ["Sa", "So", "Mo", "Di", "Mi", "Do", "Fr"];
|
||||
for (let day = 1; day <= 31; day += 1) {
|
||||
const col = day + 1;
|
||||
august.getCell(0, col).values = [[day]];
|
||||
august.getCell(1, col).values = [[weekdayNames[(day - 1) % 7]]];
|
||||
august.getCell(2, col).clear({ applyTo: "contents" });
|
||||
august.getCell(3, col).clear({ applyTo: "contents" });
|
||||
}
|
||||
august.getRange("A1:B5").copyFrom(october.getRange("A1:B5"), "all");
|
||||
august.getRange("AH1:AH5").copyFrom(october.getRange("AH1:AH5"), "all");
|
||||
august.getRange("AH4").formulas = [["=SUM(C4:AG4)"]];
|
||||
august.getRange("AH5").formulas = [["=AH4"]];
|
||||
|
||||
// Recreate the supplied visual grammar on the newly added sheet. Range copies
|
||||
// preserve content/formulas but not the source sheet's merged cells or widths.
|
||||
august.mergeCells("A3:B3");
|
||||
august.mergeCells("A4:B4");
|
||||
august.mergeCells("A5:AG5");
|
||||
august.getRange("A1:AH5").format = {
|
||||
font: { name: "Carlito", size: 11, color: "#000000" },
|
||||
verticalAlignment: "center",
|
||||
horizontalAlignment: "center",
|
||||
borders: { preset: "all", style: "thin", color: "#B7B7B7" },
|
||||
};
|
||||
august.getRange("A1:B1").format = {
|
||||
fill: "#00E640",
|
||||
font: { name: "Carlito", size: 12, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
};
|
||||
august.getRange("A2:B2").format = {
|
||||
fill: "#D9EAD3",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
};
|
||||
august.getRange("A3:B4").format = {
|
||||
fill: "#E7E6E6",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
verticalAlignment: "center",
|
||||
};
|
||||
august.getRange("A5:AH5").format = {
|
||||
fill: "#D9EAD3",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
};
|
||||
august.getRange("C1:AG2").format = {
|
||||
fill: "#FFFACD",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
};
|
||||
august.getRange("C3:AG4").format = {
|
||||
fill: "#FFFFFF",
|
||||
font: { name: "Carlito", size: 11, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
verticalAlignment: "top",
|
||||
};
|
||||
for (const weekend of ["C1:D4", "J1:K4", "Q1:R4", "X1:Y4", "AE1:AF4"]) {
|
||||
august.getRange(weekend).format = {
|
||||
fill: "#2E65A3",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#FFFFFF" },
|
||||
horizontalAlignment: "center",
|
||||
verticalAlignment: "center",
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
august.getRange("AH1:AH4").format = {
|
||||
fill: "#FFF2CC",
|
||||
font: { name: "Carlito", size: 11, bold: true, color: "#000000" },
|
||||
horizontalAlignment: "center",
|
||||
};
|
||||
august.getRange("A1:B5").format.columnWidthPx = 110;
|
||||
august.getRange("C1:AG5").format.columnWidthPx = 108;
|
||||
august.getRange("AH1:AH5").format.columnWidthPx = 118;
|
||||
august.getRange("A1:AH1").format.rowHeightPx = 22;
|
||||
august.getRange("A2:AH2").format.rowHeightPx = 22;
|
||||
august.getRange("A3:AH3").format.rowHeightPx = 96;
|
||||
august.getRange("A4:AH5").format.rowHeightPx = 24;
|
||||
|
||||
const augustEntries = {
|
||||
24: { task: "Repo-Import; PingCastle-Mapping; Dashboard; PostgreSQL/CI", hours: 7 },
|
||||
26: { task: "UI-Anpassungen; Purple Knight/PingCastle HTML-Import", hours: 7.5 },
|
||||
};
|
||||
for (const [dayText, entry] of Object.entries(augustEntries)) {
|
||||
const col = Number(dayText) + 1;
|
||||
august.getCell(2, col).values = [[entry.task]];
|
||||
august.getCell(3, col).values = [[entry.hours]];
|
||||
}
|
||||
august.getRange("C3:AG3").format.wrapText = true;
|
||||
august.getRange("C3:AG3").format.verticalAlignment = "top";
|
||||
august.getRange("C3:AG3").format.rowHeightPx = 96;
|
||||
august.getRange("C4:AG4").format.numberFormat = "0.0";
|
||||
august.getRange("AH4:AH5").format.numberFormat = "0.0";
|
||||
|
||||
const september = workbook.worksheets.getItem("September 2026");
|
||||
const septemberEntries = {
|
||||
3: { task: "Client Info; GPO-Regeln und Analysepfad; Tests", hours: 5 },
|
||||
4: { task: "GPO-Import robuster; nicht lesbare Einträge behandelt", hours: 9.5 },
|
||||
6: { task: "GPO-Auswertung testbereit; UI/Pfade; Regressionstests", hours: 9 },
|
||||
7: { task: "Scan-Navigation; GPO-Duplikate und Ergebnisabgleich", hours: 4 },
|
||||
8: { task: "GPO-Quellen konsolidiert; Historie und Tests", hours: 7 },
|
||||
9: { task: "Import-Timeout; Navigation-Performance; Tests/Build", hours: 8 },
|
||||
};
|
||||
for (const [dayText, entry] of Object.entries(septemberEntries)) {
|
||||
const col = Number(dayText) + 1;
|
||||
september.getCell(2, col).values = [[entry.task]];
|
||||
september.getCell(3, col).values = [[entry.hours]];
|
||||
}
|
||||
september.getRange("C3:AF3").format.wrapText = true;
|
||||
september.getRange("C3:AF3").format.verticalAlignment = "top";
|
||||
september.getRange("C3:AF3").format.rowHeightPx = 96;
|
||||
september.getRange("C4:AF4").format.numberFormat = "0.0";
|
||||
september.getRange("AG4:AG5").format.numberFormat = "0.0";
|
||||
|
||||
workbook.recalculate();
|
||||
|
||||
const checks = [];
|
||||
for (const range of ["August 2026!A1:AH5", "September 2026!A1:AG5"]) {
|
||||
const check = await workbook.inspect({
|
||||
kind: "table",
|
||||
range,
|
||||
include: "values,formulas",
|
||||
tableMaxRows: 5,
|
||||
tableMaxCols: 40,
|
||||
maxChars: 12000,
|
||||
});
|
||||
checks.push(check.ndjson);
|
||||
}
|
||||
console.log(checks.join("\n"));
|
||||
|
||||
const errors = await workbook.inspect({
|
||||
kind: "match",
|
||||
searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A|#NUM!|#NULL!|#SPILL!|#CALC!",
|
||||
options: { useRegex: true, maxResults: 300 },
|
||||
summary: "final formula error scan",
|
||||
});
|
||||
console.log(errors.ndjson);
|
||||
|
||||
for (const sheetName of ["August 2026", "September 2026"]) {
|
||||
const preview = await workbook.render({ sheetName, autoCrop: "all", scale: 1, format: "png" });
|
||||
await fs.writeFile(`${outputDir}/updated_${sheetName.replace(/[^a-z0-9_-]+/gi, "_")}.png`, new Uint8Array(await preview.arrayBuffer()));
|
||||
}
|
||||
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const output = await SpreadsheetFile.exportXlsx(workbook);
|
||||
await output.save(outputPath);
|
||||
console.log(`OUTPUT\t${outputPath}`);
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const sourcePath = "C:/Users/milu/OneDrive - Exxeta/Dokumente/Assessor Zeiterfassung/ASSESSOR_Zeiterfassung_Sep2026_Jul2027_Form_wie_vorher.xlsx";
|
||||
const outputPath = "C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/outputs/01a089e9-f688-7f21-87c9-db223e98841f/ASSESSOR_Zeiterfassung_Aug2026_Jul2027_aktualisiert.xlsx";
|
||||
const outputDir = "C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/outputs/01a089e9-f688-7f21-87c9-db223e98841f";
|
||||
|
||||
const source = await SpreadsheetFile.importXlsx(await FileBlob.load(sourcePath));
|
||||
const result = await SpreadsheetFile.importXlsx(await FileBlob.load(outputPath));
|
||||
|
||||
const unchanged = [];
|
||||
for (const sheet of source.worksheets.items) {
|
||||
if (sheet.name === "September 2026") continue;
|
||||
const sourceUsed = sheet.getUsedRange();
|
||||
const resultSheet = result.worksheets.getItem(sheet.name);
|
||||
const resultUsed = resultSheet.getUsedRange();
|
||||
unchanged.push({
|
||||
sheet: sheet.name,
|
||||
address: sourceUsed?.address ?? null,
|
||||
sameAddress: sourceUsed?.address === resultUsed?.address,
|
||||
sameValues: JSON.stringify(sourceUsed?.values ?? []) === JSON.stringify(resultUsed?.values ?? []),
|
||||
sameFormulas: JSON.stringify(sourceUsed?.formulas ?? []) === JSON.stringify(resultUsed?.formulas ?? []),
|
||||
});
|
||||
}
|
||||
console.log(JSON.stringify({ unchanged }, null, 2));
|
||||
|
||||
const summary = await result.inspect({
|
||||
kind: "sheet,table,formula",
|
||||
maxChars: 14000,
|
||||
tableMaxRows: 5,
|
||||
tableMaxCols: 40,
|
||||
options: { maxResults: 100 },
|
||||
});
|
||||
console.log(summary.ndjson);
|
||||
|
||||
const errors = await result.inspect({
|
||||
kind: "match",
|
||||
searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A|#NUM!|#NULL!|#SPILL!|#CALC!",
|
||||
options: { useRegex: true, maxResults: 300 },
|
||||
summary: "saved-file formula error scan",
|
||||
});
|
||||
console.log(errors.ndjson);
|
||||
|
||||
for (const sheetName of ["August 2026", "September 2026"]) {
|
||||
const preview = await result.render({ sheetName, autoCrop: "all", scale: 1, format: "png" });
|
||||
await fs.writeFile(`${outputDir}/saved_${sheetName.replace(/[^a-z0-9_-]+/gi, "_")}.png`, new Uint8Array(await preview.arrayBuffer()));
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,120 @@
|
|||
{
|
||||
"surface": "report",
|
||||
"manifest": {
|
||||
"version": 1,
|
||||
"surface": "report",
|
||||
"title": "ASSESSOR – Ist-Zustand",
|
||||
"description": "High-Level-Ist-Zustand des aktuellen ASSESSOR aus Quellcode und Projektdokumentation.",
|
||||
"generatedAt": "2026-09-09T12:00:00+02:00",
|
||||
"blocks": [
|
||||
{
|
||||
"id": "title",
|
||||
"type": "markdown",
|
||||
"body": "# ASSESSOR – Ist-Zustand"
|
||||
},
|
||||
{
|
||||
"id": "executive-summary",
|
||||
"type": "markdown",
|
||||
"body": "## Executive Summary\n\n- ASSESSOR ist aktuell eine lokale Browseranwendung mit Node-Backend und PostgreSQL; Oberfläche und API laufen same-origin.\n- Standardberichte werden aus fünf Formatfamilien verarbeitet: CSV/Text, HTML, Excel, JSON und XML.\n- Eigene Profile bestehen für Purple Knight, PingCastle, CIS-CAT Pro und Maester; unbekannte strukturierte Quellen laufen über das generische Profil.\n- GPO-Backups und Microsoft Security Baselines werden über einen separaten spezialisierten Analysepfad verarbeitet.\n- Sichtbares Ergebnis sind vereinheitlichte Findings, Evidenzen, Umgebungsdaten und Policy-Vergleiche in Client Info, Work, Result DB und Pathfinder."
|
||||
},
|
||||
{
|
||||
"id": "inputs",
|
||||
"type": "markdown",
|
||||
"body": "## Unterstützte Eingaben\n\n- **CSV/Text:** .csv und .txt; Komma, Semikolon, Tabulator und Pipe.\n- **HTML:** .html und .htm; Tabellen und erkannte statische Berichtsdaten.\n- **Excel:** .xlsx und .xls; Arbeitsblätter werden übernommen, Formeln nicht ausgeführt.\n- **JSON:** .json; Objekte und Arrays werden tabellarisch strukturiert.\n- **XML:** .xml; hierarchische Strukturen ohne aktive externe XML-Konstrukte.\n- **Scannerprofile:** Purple Knight, PingCastle, CIS-CAT Pro, Maester und Generic.\n- **GPO/Baseline:** GPO-Backup als ZIP oder unterstützte Ordnerauswahl; Microsoft Security Baseline als ZIP.\n- **GPO-Artefakte:** Registry.pol, GptTmpl.inf, audit.csv sowie ausgewählte Group Policy Preferences für Registry, Services, Laufwerkszuordnungen und lokale Benutzer/Gruppen."
|
||||
},
|
||||
{
|
||||
"id": "scanner-specifics",
|
||||
"type": "markdown",
|
||||
"body": "## Scannerbezogene Besonderheiten\n\n- **Purple Knight:** HTML und tabellarische Exporte; Findings, Detailtabellen, Zusammenfassungen und ausgewählte Umgebungskennzahlen.\n- **PingCastle:** HTML liefert Risk Rules und Details; XML liefert eigenständige strukturierte Berichtsdaten und Kontext, aber nicht die vollständige HTML-Findingansicht.\n- **CIS-CAT Pro:** HTML mit Benchmark-Ergebnissen, Regelstatus, Evidenztabellen, Summary-Abgleich und System-/Benchmarkkontext.\n- **Maester:** HTML mit sicher lesbarem statischem Ergebnisobjekt sowie tabellarische/Excel-Ausgaben; Tests, Status, Ergebnistext und Berichtskontext.\n- **Generic:** strukturierte Tabellen/Objekte; fachliche Feldzuordnung nur bei ausreichender Erkennung oder Nutzerbestätigung."
|
||||
},
|
||||
{
|
||||
"id": "flow",
|
||||
"type": "markdown",
|
||||
"body": "## Datenstrecke: sichtbarer Start- bis Endpunkt\n\n1. **Start:** Berichtdateien im Data Hub auswählen oder GPO-/Baseline-Import starten.\n2. **Eingangskontrolle:** Format, Signatur, Ressourcenlimits und unsichere Inhalte lokal prüfen.\n3. **Strukturierung:** Formatadapter erzeugt ein gemeinsames Zwischenmodell.\n4. **Erkennung:** Tool, Ergebnistabelle und fachliche Felder mit Konfidenz bestimmen; unsichere Zuordnungen bestätigen lassen.\n5. **Normalisierung:** Status, Severity, Titel, Kategorien, Evidenz, Referenzen und Kontext in das kanonische Modell überführen; Originalwerte und Provenienz erhalten.\n6. **Persistenz:** Bestätigten Import atomar als Projekt, Assessment, Scanlauf, Findings, Evidenz, Diagnosen und Historie in PostgreSQL speichern.\n7. **Endpunkt:** Gewählten Client/Timeslot in Client Info, Work, Result DB und Pathfinder anzeigen."
|
||||
},
|
||||
{
|
||||
"id": "capabilities",
|
||||
"type": "markdown",
|
||||
"body": "## Weitere Fähigkeiten\n\n- **Datahub:** Clients/Projekte, Mehrdatei-Vorschau, Tool-/Formatprüfung, Importdiagnosen, Dublettenhinweise, Reimport/Ersatzläufe, Historie und Timeslots.\n- **Client Info:** Umgebungsfakten, Overall Score, Security Domains, deviation-basierte Priority Findings, Assessment Gaps, Statuskontext und Source Overview.\n- **Work:** Reports als Standardansicht sowie Matrix, Grid und Raw Data; Suche, Sortierung und dynamische Filter.\n- **Finding-Bearbeitung:** Bookmark, Reviewstatus, Priorität, Owner/Team, Fälligkeit, Voraussetzungen, Rollen, Next Step, Evidenzkriterien, Kommentar und Reportmarkierungen.\n- **Vergleich:** Neue, fortbestehende, behobene und wiederkehrende Befunde über Scanläufe/Timeslots.\n- **Result DB:** Vereinheitlichte Scanner-, Kontext- und Policy-Ergebnisse mit Facetten, Details und JSONL-Export.\n- **Pathfinder:** Deviation-basierter Graph mit Kategorie-, Asset-, MITRE ATT&CK- und D3FEND-Beziehungen.\n- **GPO-Analyse:** Baselinevergleich mit Misconfigured, Match/Passed, Not configured, Conflicts, Duplicates und Review required.\n- **Exporte:** Sichere CSV, ASSESSOR-Snapshot als JSON, Result DB als JSONL und HTML-basierter Aktions-/Dashboardexport.\n- **Betrieb:** Backend-Health, DB-Strukturansicht, versionierte Migrationen und technische Verarbeitungsdiagnosen."
|
||||
},
|
||||
{
|
||||
"id": "security",
|
||||
"type": "markdown",
|
||||
"body": "## Sicherheit und Betriebsgrenzen\n\n- Lokale Verarbeitung ohne externe Uploads, Telemetrie, CDN-Abhängigkeiten oder automatische externe APIs.\n- Importiertes HTML wird nicht ausgeführt; aktive Inhalte werden entfernt.\n- XML mit DTD/ENTITY/SYSTEM/PUBLIC, Excel mit VBA oder verschlüsselten Archiven und nicht statisch lesbare Maester-Ausdrücke werden abgelehnt.\n- Standardgrenzen: 100 MiB je Datei, 140 MiB je Request und 50 Dateien je Batch; zusätzlich Zeit-, Speicher-, Mengen- und Parallelitätsgrenzen.\n- PostgreSQL ist die kanonische lokale Datenhaltung; kein stiller Fallback bei konfigurierter, aber nicht erreichbarer Datenbank.\n- Noch kein freigegebener Mehrbenutzer-Firmenbetrieb: HTTPS, SSO, Rollen und Autorisierung gehören zu Phase 14."
|
||||
},
|
||||
{
|
||||
"id": "maturity",
|
||||
"type": "markdown",
|
||||
"body": "## Reifegrad des Umsetzungsplans\n\n- **15 Phasen/Teilphasen abgeschlossen:** Phase 0 bis 13A.\n- **2 Teilphasen in Arbeit:** 13B GPO/Baseline-Analyse und 13C Scanner Context/Environment Inventory.\n- **1 Phase offen:** Phase 14 Mehrbenutzer- und Firmenbetrieb."
|
||||
},
|
||||
{
|
||||
"id": "phase-chart-block",
|
||||
"type": "chart",
|
||||
"chartId": "phase-status"
|
||||
},
|
||||
{
|
||||
"id": "next-steps",
|
||||
"type": "markdown",
|
||||
"body": "## Empfohlene nächste Schritte\n\n- Phase 13B gegen die definierten GPO-/Baseline-Akzeptanzkriterien abschließen.\n- Phase 13C für alle unterstützten Scannerprofile vervollständigen.\n- Vor Firmenbetrieb Phase 14 mit HTTPS, SSO, Rollen, Autorisierung, Betriebsüberwachung sowie Backup/Restore umsetzen.\n- Format- und Profilkompatibilität weiter mit synthetischen Referenzberichten und Vollständigkeitsabgleichen absichern."
|
||||
},
|
||||
{
|
||||
"id": "questions",
|
||||
"type": "markdown",
|
||||
"body": "## Offene Fragen\n\n- Welche Scanner oder Reportvarianten sollen als Nächstes ein eigenes Profil erhalten?\n- Welche zusätzlichen GPO-Artefaktarten sind fachlich zwingend?\n- Wird zusätzlich eine Capability-Matrix „Format × Tool × sichtbare Felder“ benötigt?"
|
||||
},
|
||||
{
|
||||
"id": "caveats",
|
||||
"type": "markdown",
|
||||
"body": "## Caveats\n\n- Stand: aktueller Workspace am 09.09.2026; Basis sind Quellcode, Sicherheitsdokumentation und Umsetzungsplan, keine Kundendaten.\n- „Parsebar“ heißt strukturell lesbar; vollständige Findings setzen ein passendes Profil oder bestätigtes Mapping voraus.\n- GPO/Baseline und Environment sind nutzbar, die zugehörigen Phasen 13B und 13C bleiben aber in Arbeit.\n- High-Level-Ist-Zustand, kein Feld-für-Feld- oder Parser-Testnachweis."
|
||||
}
|
||||
],
|
||||
"charts": [
|
||||
{
|
||||
"id": "phase-status",
|
||||
"title": "Umsetzungsstatus nach Phasen",
|
||||
"type": "bar",
|
||||
"dataset": "phase_status",
|
||||
"encodings": {
|
||||
"x": { "field": "status", "type": "nominal", "title": "Status" },
|
||||
"y": { "field": "anzahl", "type": "quantitative", "title": "Anzahl Phasen/Teilphasen" }
|
||||
},
|
||||
"options": { "orientation": "vertical", "grouping": "grouped", "showLegend": false },
|
||||
"source": {
|
||||
"id": "phase-status-values",
|
||||
"label": "Aus dem ASSESSOR-Umsetzungsplan abgeleitete Statuszählung",
|
||||
"query": {
|
||||
"engine": "static-sql",
|
||||
"description": "Zählt die dokumentierten Phasen und Teilphasen nach Status; Grundlage: docs/IMPLEMENTATION_PLAN.md.",
|
||||
"sql": "SELECT status, anzahl, umfang FROM (VALUES ('Abgeschlossen', 15, 'Phase 0–13A'), ('In Arbeit', 2, 'Phase 13B und 13C'), ('Offen', 1, 'Phase 14')) AS phase_status(status, anzahl, umfang)"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{ "id": "implementation-plan", "label": "ASSESSOR Umsetzungsplan", "path": "docs/IMPLEMENTATION_PLAN.md" },
|
||||
{ "id": "import-code", "label": "Importengine und Toolprofile", "path": "src/importers; src/profiles" },
|
||||
{ "id": "app-code", "label": "Anwendungsbereiche und sichtbare Datenprojektionen", "path": "src/app; src/features" },
|
||||
{ "id": "backend-code", "label": "Backend, GPO-Analyse und Persistenz", "path": "server; src/persistence; src/api" },
|
||||
{ "id": "security-docs", "label": "Sicherheitsmodell und Importgrenzen", "path": "SECURITY.md; docs/IMPORT_SECURITY_LIMITS.md" }
|
||||
]
|
||||
},
|
||||
"snapshot": {
|
||||
"version": 1,
|
||||
"generatedAt": "2026-09-09T12:00:00+02:00",
|
||||
"status": "ready",
|
||||
"datasets": {
|
||||
"phase_status": [
|
||||
{ "status": "Abgeschlossen", "anzahl": 15, "umfang": "Phase 0–13A" },
|
||||
{ "status": "In Arbeit", "anzahl": 2, "umfang": "Phase 13B und 13C" },
|
||||
{ "status": "Offen", "anzahl": 1, "umfang": "Phase 14" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"sources": [
|
||||
{ "id": "implementation-plan", "label": "ASSESSOR Umsetzungsplan", "path": "docs/IMPLEMENTATION_PLAN.md" },
|
||||
{ "id": "import-code", "label": "Importengine und Toolprofile", "path": "src/importers; src/profiles" },
|
||||
{ "id": "app-code", "label": "Anwendungsbereiche und sichtbare Datenprojektionen", "path": "src/app; src/features" },
|
||||
{ "id": "backend-code", "label": "Backend, GPO-Analyse und Persistenz", "path": "server; src/persistence; src/api" },
|
||||
{ "id": "security-docs", "label": "Sicherheitsmodell und Importgrenzen", "path": "SECURITY.md; docs/IMPORT_SECURITY_LIMITS.md" }
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
# PingCastle-Importprüfung
|
||||
|
||||
## Nachtrag: Punkte 1–3 umgesetzt und verifiziert
|
||||
|
||||
9. September 2026, PingCastle-Profil **1.5.0**, Extractor **2.2.0**,
|
||||
HTML/XML-Formatadapter **1.1.0**. Der ursprüngliche Prüfstand darunter bleibt
|
||||
als historische Diagnose erhalten.
|
||||
|
||||
- XML: Alle 403 Attribute außerhalb der RiskRules sind einzeln mit Originalwert
|
||||
und Attributpfad vorhanden, außerdem alle 70 explizit leeren Blätter.
|
||||
Zusammen mit den 664 Textwerten entstehen 1.137 Kontextbeobachtungen.
|
||||
- HTML: Alle 125 Tabellen und 1.513 Kontextbeobachtungen bleiben erhalten.
|
||||
Die 47 verunreinigten Kontextzellen, 3 Finding-Details und 16 verknüpften
|
||||
Detailzell-Vorkommen enthalten keine technischen Attributreste mehr.
|
||||
Eine unabhängige HTML-Strukturzählung bestätigt unveränderte Elementanzahlen
|
||||
vor und nach dem Schutz zitierter Attribute.
|
||||
- Fehlende Rule IDs scheitern explizit. Dubletten sowie ungültige oder
|
||||
abweichende Kategorie-Trefferzahlen erzeugen Diagnosen; die bisherige erste
|
||||
Darstellung bleibt erhalten. Originalzähler und errechnete eindeutige Treffer
|
||||
stehen in einem zusätzlichen Bilanzabschnitt: insgesamt 18 statt 17 Abschnitte,
|
||||
ohne die Environment- oder Findingzahl zu erhöhen.
|
||||
- Beide Formate liefern weiterhin dieselben 37 Regeln mit identischem Status,
|
||||
Regelpunkten und abgeleiteter Severity. Bekannte Quellgrenzen bleiben sichtbar:
|
||||
ein unaufgelöster HTML-Verweis und die XML-Results-Warnung.
|
||||
|
||||
21 neue synthetische Regressionen; `npm test`: 1.096 bestanden, acht optionale
|
||||
PostgreSQL-Tests übersprungen, Sicherheits-/Designprüfung erfolgreich.
|
||||
`npm run build`: Client und Server erfolgreich, bestehender Bundlegrößenhinweis.
|
||||
Der produktive Parse-only-HTTP-Pfad bestätigt Profil und Zahlen; App, Backend
|
||||
und Proxy antworten mit HTTP 200. Der kanonische PostgreSQL-Datastore wurde vor
|
||||
dem Backend-Neustart geprüft; die Repositoryrevision blieb während des
|
||||
Parse-Abgleichs unverändert. Keine Importdaten gespeichert oder ersetzt,
|
||||
keine Schemaänderung. Temporäre Dummy-Prüfdatei entfernt.
|
||||
|
||||
Bestehende Importe benötigen einen expliziten Reimport. Punkte 4–5
|
||||
(semantisches XML-Kontextmapping und Control-Path-JSON) sind nicht Teil dieser
|
||||
Freigabe und bleiben offen. Die Schutzregeln stehen in `AGENTS.md`,
|
||||
`SECURITY.md` und `docs/IMPORT_SECURITY_LIMITS.md`.
|
||||
|
||||
## Historischer Prüfstand vor der Korrektur
|
||||
|
||||
Stand: 9. September 2026. Assessor-Profil 1.4.0, Extractor 2.1.0. Prüfung, keine Implementierung oder Bestandsänderung. Einordnung: Phase 13A/13C.
|
||||
|
||||
## Ergebnis
|
||||
|
||||
**Die 37 ausgelösten Regeln sind vollständig und korrekt übernommen. Der gesamte Berichtskontext ist noch nicht vollständig beziehungsweise unverfälscht nutzbar.**
|
||||
|
||||
HTML und XML ergeben jeweils 37 eindeutige Regel-IDs und 37 kanonische Vorkommen. Titel, Regelpunkte, Status, Domain-Risiko und zugeordneter Kategoriescore stimmen für jede Regel überein. Browser- und Server-Adapter liefern dieselben Kandidaten und Kontextbeobachtungen. Beide kanonischen Modelle bestehen die Strukturvalidierung.
|
||||
|
||||
Die unabhängig aus den Originalen gezählten Treffer sind Stale Objects 10, Privileged Accounts 7, Trusts 0 und Anomalies 20. Die daneben genannten 187 Katalogregeln sind **keine 187 vorhandenen Einzeltestergebnisse**. Assessor leitet aus den Regelpunkten in beiden Formaten 8 Medium, 13 Low und 16 Info ab; das ist keine native PingCastle-Severity. Native Risikowerte: Domain 100, Stale Objects 31, Privileged Accounts 85, Trusts 0 und Anomalies 100.
|
||||
|
||||
## Was zu korrigieren wäre
|
||||
|
||||
### P1: XML-Attribute und explizite Leere erhalten
|
||||
|
||||
Außerhalb der RiskRules enthält XML 758 Blattelemente: 664 mit nichtleerem Text, 24 ohne Text mit Attributen und 70 ohne beides. Nur die 664 Textwerte werden kanonische Kontextbeobachtungen. Die 24 attributtragenden Elemente bestehen aus 16 CertificateTemplateRights, 4 Verteilungsdatensätzen und 4 Control-Path-Analysewerten. Auch die 70 ausdrücklich leeren Elemente fehlen als Zustände.
|
||||
|
||||
Das betrifft die strukturierte Projektion, nicht die Existenz des ursprünglichen Rohdokuments. Attribute werden im generischen XML-Zwischenmodell noch geführt. `xmlCandidates` verwirft jedoch Kontextknoten ohne Text und übernimmt die Attribute der übrigen Knoten ebenfalls nicht in die Kontextwerte. Folge: Berechtigungs-/Verteilungsinformationen und die Unterscheidung zwischen leer und nicht erhoben sind nicht vollständig nutzbar.
|
||||
|
||||
Korrektur: Originalattribute mit genauen Quellpfaden erhalten, Attribute separat typisieren und leere Sammlungen ausdrücklich kennzeichnen. Sicherheit der Feststellung: hoch.
|
||||
|
||||
### P1: HTML-Zelltext und Finding-Details sauber extrahieren
|
||||
|
||||
47 Kontextzellen enthalten technische Attributreste. Zusätzlich sind 3 Finding-Details sowie 16 Zellen verknüpfter Detailtabellen betroffen; die 16 zählen Vorkommen und nicht zwingend unterschiedliche Quellzellen.
|
||||
|
||||
Die lexikalischen Tag-/Zellregulärausdrücke sind gegenüber spitzen Klammern innerhalb zitierter Attribute nicht robust. Eine ausschließlich präsentative Bereinigung repariert keine bereits verunreinigten strukturierten Werte.
|
||||
|
||||
Korrektur: Attributgrenzen zuverlässig erkennen, sichtbaren Text vom Originalmarkup trennen und dieselbe Regel für Kontext und Finding-Details verwenden. Sicherheit: hoch.
|
||||
|
||||
### P1: Stille Nullbefunde, falsche Zähler und Dubletten diagnostizieren
|
||||
|
||||
Die Originaldateien haben keine fehlenden IDs oder widersprüchlichen Trefferzahlen. Drei zusätzliche synthetische Gegenproben belegen jedoch fehlende Schutzprüfungen:
|
||||
|
||||
- Ein angekündigter Treffer mit leerer Rule ID: 0 Kandidaten, `complete`, keine Diagnose.
|
||||
- Zwei angekündigte Treffer mit einem Regelblock: 1 Kandidat, `complete`, keine Diagnose.
|
||||
- Zwei Regelblöcke mit derselben ID: still auf 1 Kandidaten reduziert, `complete`, keine Diagnose.
|
||||
|
||||
Korrektur: Trefferzahlen je Kategorie gegen Einzelregeln abgleichen; fehlende IDs und doppelte IDs explizit behandeln. Keine Regeln verwerfen, um eine Summe passend zu machen. Sicherheit: hoch; die Relevanz für künftige Exporte ist durch synthetische Fälle belegt, nicht als Fehler dieses Dummy-Paares behauptet.
|
||||
|
||||
### P2: XML auf denselben Kontextvertrag wie HTML abbilden
|
||||
|
||||
HTML liefert 1.513 Beobachtungen in 17 Abschnitten, XML 664 in einem Sammelabschnitt. Diese Mengen haben unterschiedliche Körnung und sind keine unmittelbar vergleichbaren Vollständigkeitsquoten.
|
||||
|
||||
Beim XML liegen alle Eigenschaften unter `pingcastle.xml.*`. Gemeinsame Eigenschaften wie `domain.fqdn`, `risk.domain` und `report.engine_version` fehlen. Die entsprechenden Client-Info-Selektoren können die vorhandenen Quelldaten daher nicht gleichwertig nutzen. Die kanonische Toolversion bleibt beim XML leer.
|
||||
|
||||
Korrektur: fachlich äquivalente HTML-/XML-Werte auf dieselben Eigenschaften abbilden und zugleich ursprüngliche Werte und Pfade behalten. Sicherheit: hoch.
|
||||
|
||||
### P2: Control-Path-JSON erhalten
|
||||
|
||||
HTML enthält 27 gültige `application/json`-Blöcke: 26 Graphen mit insgesamt 255 Knoten- und 229 Kanten-Vorkommen sowie ein Beziehungserklärungs-Wörterbuch. Diese Strukturen werden vom PingCastle-Extractor nicht eigenständig übernommen. Die Summen sind **keine deduplizierte AD-Assetanzahl**. Vorhandene Tabellen können Teile derselben Information enthalten, ersetzen aber nicht die Knoten-Kanten-Beziehungen.
|
||||
|
||||
Korrektur: ausschließlich statisches JSON lesen, Struktur und Referenzen validieren und mit Provenienz erhalten. Keine Berichtsskripte ausführen. Eine zusätzliche graphische Darstellung ist eine gesonderte Produktentscheidung. Sicherheit: hoch.
|
||||
|
||||
## Kein Assessor-Fehler: fehlendes Ankerziel
|
||||
|
||||
Ein HTML-Detailverweis auf `HardenedPaths` hat im Original kein entsprechendes Ziel. Assessor meldet ihn korrekt als unaufgelöst. Es darf kein vermeintlich passender Zielabschnitt erfunden werden.
|
||||
|
||||
Bei Textvergleichen festgestellte Absatzunterschiede wurden nicht pauschal als Inhaltsverlust ausgegeben: überwiegend handelte es sich um Whitespace oder zusätzliche Inhalte gegenüber dem ersten DOM-Absatz.
|
||||
|
||||
## Verifikation und Grenzen
|
||||
|
||||
- Unabhängige native Regelzählung und ID-Abgleich, zusätzlich XML-Vergleich sämtlicher 37 Rationale-, Points-, Category- und Model-Werte.
|
||||
- Produktive Importpipeline durch Browser-/Server-Adapter und kanonische Modellvalidierung.
|
||||
- Laufendes Backend über den nicht speichernden Parse-Endpunkt: beide Dateien HTTP 200, je 37 Kandidaten; HTML 1.513 Kontextbeobachtungen/17 Abschnitte, XML 664/1. Repositoryrevision vor und nach der Prüfung unverändert.
|
||||
- 34 vorhandene fokussierte Tests bestanden; ein zusätzlicher temporärer Test führte die drei oben beschriebenen Gegenproben aus. Temporäre Testdateien entfernt.
|
||||
- Ein erweiterter unabhängiger DOM-Auditlauf überschritt unter Last sein Zeitlimit, nachdem er die Auswertung ausgegeben hatte. Ein vorheriger Kernabgleich war erfolgreich. Keine Performance-Aussage aus diesen Auditlaufzeiten.
|
||||
- Keine vollständige visuelle UI-Abnahme, keine Aussage über alle PingCastle-Versionen, keine Migration, kein Reimport und keine Produktionscodeänderung. Aus einem Berichtspaar lässt sich kein zeitlicher Trend ableiten.
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
Zuerst XML-Werterhalt, HTML-Textbereinigung und Vollständigkeitsdiagnosen; anschließend einheitliche Kontextabbildung und Control-Path-Daten. Danach synthetische Regressionen, denselben Dummy erneut abgleichen, Verarbeitung versionieren und erst auf Freigabe bestehende Importe ersetzen. HTML und XML nicht ungeprüft als doppelte Scans importieren oder automatisch miteinander verschmelzen.
|
||||
|
||||
## Quellen und Reproduzierbarkeit
|
||||
|
||||
- [Freigegebenes HTML](<C:/Users/milu/OneDrive - Exxeta/OC_SOC - General/Projekt Assessor/Dummy-Scans/PingCastle/ad_hc_sevenkingdoms.local.html>)
|
||||
- [Freigegebenes XML](<C:/Users/milu/OneDrive - Exxeta/OC_SOC - General/Projekt Assessor/Dummy-Scans/PingCastle/ad_hc_sevenkingdoms.local.xml>)
|
||||
- [Native Quellextraktion](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/pingcastle-source-extractor.ts:586>)
|
||||
- [XML-Kontextprojektion](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/pingcastle-source-extractor.ts:1229>)
|
||||
- [Lexikalische HTML-Extraktion](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/html-extraction.ts:404>)
|
||||
- [Strukturierte Prüfergebnisse und XML-Zählkern](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/reports/pingcastle-audit-2026-09-09/artifact.json>)
|
||||
|
||||
Der Datenqualitäts-Skill führte zur unabhängigen Quellenzählung statt einer bloßen Erfolgsprüfung. Der zusätzliche Build-report-/Visualize-data-Ausgabeweg ist blockiert: Gehostete Ausgabe ist wegen der projektspezifischen Local-only-Regel ausgeschlossen. Der lokale Portable-Builder verlangt für Diagramme tatsächlich SQL-förmige Provenienz und lehnt die hier ausgeführte JavaScript/XML-Auswertung ab. Eine SQL-Abfrage wurde nicht erfunden. Daher liegt dieses Markdown-Prüfprotokoll vor, kein gerenderter HTML-Prüfbericht. Dies betrifft ausschließlich die Darstellung dieses Prüfprotokolls, **nicht** die erfolgreich geprüfte PingCastle-HTML-Importfunktion. Es wurden keine Daten extern hochgeladen.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"surface": "report",
|
||||
"manifest": {
|
||||
"version": 1,
|
||||
"surface": "report",
|
||||
"title": "PingCastle: Importprüfung",
|
||||
"generatedAt": "2026-09-09T11:30:00Z",
|
||||
"sources": [
|
||||
{"id":"pair","label":"Freigegebener PingCastle-Dummy und Assessor-Import","query":{"description":"Abgleich von ad_hc_sevenkingdoms.local.html und ad_hc_sevenkingdoms.local.xml mit der unveränderten Assessor-Pipeline. Native Quellzählung, Browser-/Server-Adapter und kanonisches Modell; Prüfung am 9. September 2026. Profil 1.4.0, Extractor 2.1.0."}},
|
||||
{"id":"xml","label":"ad_hc_sevenkingdoms.local.xml","query":{"language":"javascript","engine":"Node.js / JSDOM","sql":"const leafNodes = [...document.querySelectorAll('*')].filter(element => !element.children.length && !element.closest('RiskRules'));\nconst total = leafNodes.length;\nconst textLeaves = leafNodes.filter(element => element.textContent?.trim()).length;\nconst attributeOnlyLeaves = leafNodes.filter(element => !element.textContent?.trim() && element.attributes.length);\nconst attributeKinds = Object.fromEntries([...new Set(attributeOnlyLeaves.map(element => element.tagName))].map(key => [key, attributeOnlyLeaves.filter(element => element.tagName === key).length]));\nconst emptyLeaves = leafNodes.filter(element => !element.textContent?.trim() && !element.attributes.length).length;","description":"Ausgeführter Zählkern; document ist das inerte XML-DOM der Datei ad_hc_sevenkingdoms.local.xml. 758 Blattelemente außerhalb RiskRules: 664 mit nichtleerem Text, 24 ohne Text mit Attributen, 70 ohne Text und Attribute. Vergleich mit den 664 kanonischen Kontextbeobachtungen. Diagramm gruppiert HealthCheckCertificateTemplateRights separat (16), Dist und CompromiseGraphAnomalyAnalysisData gemeinsam (4 + 4). Namespace-Deklarationen der Wurzel sind nicht Teil dieser Blattzählung."}},
|
||||
{"id":"html","label":"ad_hc_sevenkingdoms.local.html","query":{"description":"Unabhängige inerte DOM-Prüfung und JSON.parse ausschließlich für application/json-Blöcke. Keine Ausführung von Berichtsskripten. Vergleich mit Kandidaten, Berichtstabellen, Detailfeldern und Kontextprojektion."}},
|
||||
{"id":"guards","label":"Synthetische PingCastle-Gegenproben und Quellcode","query":{"description":"Temporäre Vitest-Prüfung von leerer Rule ID, deklarierten zwei Treffern bei einer Regel und zweimal derselben Rule ID. Alle drei Fälle als complete ohne Diagnose akzeptiert. Geprüfte Funktionen: htmlCandidates, xmlCandidates, contextObservations in src/importers/pingcastle-source-extractor.ts; decodeHtmlLexically in src/importers/html-extraction.ts. Temporäre Dateien nach der Prüfung entfernt."}}
|
||||
],
|
||||
"blocks": [
|
||||
{"id":"title","type":"markdown","body":"# PingCastle: Importprüfung"},
|
||||
{"id":"summary","type":"markdown","sourceId":"pair","body":"## Befunde vollständig, Zusatzdaten nicht vollständig\n\nHTML und XML liefern jeweils **37 von 37 ausgelösten Regeln**, ohne doppelte Regel-IDs und ohne Mapping-Rückfragen. Titel, Regelpunkte, Status sowie zugeordnete Domain- und Kategoriescores stimmen für alle 37 Regeln überein. Der strukturierte Kontext ist dagegen lückenhaft beziehungsweise teilweise verunreinigt. Für die Zusage, PingCastle vollständig zu übernehmen, sind weitere Korrekturen erforderlich.\n\nGeprüft wurde die vorhandene Implementierung, nicht verändert. Bestehende Importe wurden nicht ersetzt."},
|
||||
{"id":"scope","type":"markdown","sourceId":"pair","body":"## Was hier als vollständig gilt\n\nEin Befund ist eine ausgelöste RiskRule, nicht jede Regel des PingCastle-Prüfkatalogs. Das HTML nennt 10 Treffer bei Stale Objects, 7 bei Privileged Accounts, 0 bei Trusts und 20 bei Anomalies; zusammen 37. Die daneben genannten insgesamt 187 Katalogregeln dürfen nicht als 187 vorhandene Einzeltestergebnisse behandelt werden.\n\nAssessor übernimmt für beide Formate denselben Status und dieselbe abgeleitete Severity: 8 Medium, 13 Low, 16 Info. PingCastle liefert Regelpunkte, keine native Severity je Regel. Domain-Risiko 100, Stale Objects 31, Privileged Accounts 85, Trusts 0 und Anomalies 100 bleiben native Risikowerte, keine vergleichbaren ASSESSOR-Scores.\n\nKontextvollständigkeit bedeutet zusätzlich: Textwerte, Attribute, explizite Leere, Berichtsdaten und strukturierte Verknüpfungen bleiben mit Herkunft nutzbar. Die bloße Verfügbarkeit des ursprünglichen Rohdokuments erfüllt diese Bedingung nicht."},
|
||||
{"id":"xml-loss","type":"markdown","sourceId":"xml","body":"## P1: XML verliert Attributwerte und explizite Leere\n\nVon 758 Kontext-Blattelementen gelangen die 664 mit nichtleerem Text als Beobachtungen in das kanonische Modell. **24 attributtragende Blattelemente ohne Text fehlen dort**: 16 CertificateTemplateRights, 4 Verteilungsdatensätze und 4 Control-Path-Analysewerte. Dazu kommen 70 ausdrücklich leere Elemente. Dadurch gehen insbesondere Berechtigungs- und Verteilungsinformationen im strukturierten Kontext verloren; eine leere Sammlung ist nicht mehr zuverlässig von einer nicht erhobenen Sammlung unterscheidbar.\n\nDie folgende Darstellung zählt XML-Blattelemente, nicht Attribute oder Findings. Alle vier Gruppen sind disjunkt und ergeben zusammen 758. Roh-XML und generische Zwischenstruktur sind hiervon zu unterscheiden: Die Lücke liegt in der kanonischen Kontextprojektion. Sicherheit der Feststellung: hoch."},
|
||||
{"id":"xml-coverage","type":"chart","chartId":"xml-leaves"},
|
||||
{"id":"html-loss","type":"markdown","sourceId":"html","body":"## P1: HTML-Attributreste verschmutzen Kontext und Details\n\n**47 Kontextzellen** enthalten technische Attributreste. Auch 3 Finding-Details und 16 Zellen der verknüpften Ergebnistabellen sind betroffen; letztere Zahl zählt Vorkommen, nicht eindeutige Quellzellen. Die lexikalische HTML-Verarbeitung und einfache Tag-Regulärausdrücke behandeln spitze Klammern innerhalb zitierter Attribute nicht robust genug. Eine reine Anzeigebereinigung löst die Verunreinigung gespeicherter beziehungsweise normalisierter Werte nicht.\n\nEmpfehlung: Attributgrenzen sicher berücksichtigen, sichtbaren Zelltext getrennt vom ursprünglichen Markup gewinnen und beide mit Provenienz erhalten. Dieselbe Bereinigung muss für Kontext- und Finding-Details gelten. Sicherheit: hoch."},
|
||||
{"id":"graphs","type":"markdown","sourceId":"html","body":"## P2: Control-Path-Graphdaten werden nicht strukturiert übernommen\n\nDas HTML enthält 27 gültige application/json-Blöcke: **26 Graphen mit zusammen 255 Knoten- und 229 Kanten-Vorkommen** sowie ein Wörterbuch mit Beziehungserklärungen. Diese Graphstrukturen werden vom PingCastle-Extractor nicht als eigene Strukturen erhalten. Vorhandene Tabellen und Texte können Teile derselben Information enthalten, ersetzen aber keine Knoten-Kanten-Beziehungen. Die Zahlen sind keine deduplizierte Anzahl von AD-Objekten.\n\nZusätzlich bleibt ein Detailverweis auf HardenedPaths unaufgelöst. Im Quelldokument existiert kein entsprechendes Ankerziel; die vorhandene Warnung ist hier korrekt. Daraus darf kein erfundener Detailabschnitt entstehen. Sicherheit: hoch."},
|
||||
{"id":"xml-mapping","type":"markdown","sourceId":"pair","body":"## P2: XML-Kontext fehlt die gemeinsame fachliche Zuordnung\n\nHTML liefert 1.513 Kontextbeobachtungen in 17 Abschnitten, XML 664 Beobachtungen in einem Sammelabschnitt. Diese Mengen haben unterschiedliche Körnung und dürfen nicht direkt als Verlustquote verglichen werden. Beim XML liegen sämtliche Werte unter generischen pingcastle.xml-Eigenschaften. Gemeinsame Eigenschaften wie domain.fqdn, risk.domain und report.engine_version fehlen. Obwohl der Export die Werte enthält, können die darauf aufbauenden Client-Info-Selektoren sie nicht gleichwertig nutzen; auch die kanonische Toolversion bleibt beim XML leer.\n\nEmpfehlung: XML und HTML auf denselben fachlichen Kontextvertrag abbilden und zugleich die unveränderten XML-Werte samt Herkunft erhalten. Fehlende Daten niemals zu Null umdeuten. Sicherheit: hoch."},
|
||||
{"id":"guards","type":"markdown","sourceId":"guards","body":"## P1: Fehlende Regeln müssen als Fehler oder Teilverarbeitung auffallen\n\nDrei synthetische Gegenproben werden ohne Diagnose als vollständig akzeptiert: ein angekündigter Treffer mit leerer Rule ID wird zu 0 Kandidaten; zwei angekündigte Treffer mit nur einem Regelblock werden zu 1 Kandidaten; zwei Blöcke mit derselben ID werden still zu 1 Kandidaten. Die freigegebenen Originaldateien zeigen diese Abweichungen nicht. Die Gegenproben belegen eine fehlende Schutzprüfung für künftige oder beschädigte Exporte.\n\nEmpfehlung: deklarierte Trefferzahlen je Kategorie gegen unabhängig gezählte Einzelregeln prüfen, fehlende Identitäten explizit melden und Dubletten nicht kommentarlos verwerfen. Sicherheit: hoch."},
|
||||
{"id":"verification","type":"markdown","sourceId":"pair","body":"## Prüfung und Grenzen\n\nNative Regel-IDs wurden unabhängig gezählt und mit dem Import abgeglichen. Für XML stimmen außerdem sämtliche 37 Rationale-, Points-, Category- und Model-Werte mit dem Quelltext überein. Browser- und Server-Adapter erzeugen für beide Dateien dieselben Kandidaten und Kontextbeobachtungen; beide Modelle bestehen die kanonische Strukturvalidierung. Die fokussierten vorhandenen Tests bestanden; die Gegenproben demonstrieren fehlende Guards, keine bereits implementierten Fixes.\n\nUnterschiede bei HTML-Absatztexten wurden nicht pauschal als Verlust gewertet: die geprüften Differenzen waren überwiegend Whitespace oder zusätzliche Inhalte gegenüber dem ersten DOM-Absatz. Ein tiefer DOM-Auditlauf überschritt unter Last sein Zeitlimit; daraus wird kein Performance-Benchmark abgeleitet. Keine Aussage über sämtliche PingCastle-Versionen, keine Bestandsdatenmigration, kein Reimport und keine vollständige visuelle UI-Abnahme. Es liegt ein Berichtspaar vor, daher keine Zeitreihenanalyse."},
|
||||
{"id":"next","type":"markdown","body":"## Empfohlene Reihenfolge\n\n1. XML-Attribute und explizite Leere verlustfrei in den Kontext übernehmen.\n2. HTML-Zell- und Detailtext korrekt auslesen.\n3. Vollständigkeits-, Identitäts- und Dublettendiagnosen ergänzen.\n4. XML auf den gemeinsamen Kontextvertrag abbilden.\n5. Statische Control-Path-JSON-Daten samt Beziehungen erhalten.\n\nAnschließend synthetische Regressionen und denselben Dummy-Abgleich durchführen, Verarbeitung versionieren und bestehende Importe gezielt neu importieren. Vor einer Umsetzung bleibt zu entscheiden, ob Control-Path-Daten zunächst als strukturierte Evidenz genügen oder zusätzlich eine eigene Darstellung erhalten sollen. Die Parserprüfung autorisiert keine automatische Zusammenführung von HTML und XML zu einem Scan."}
|
||||
],
|
||||
"charts": [{"id":"xml-leaves","title":"XML-Kontext nach Quellstruktur","dataset":"xmlLeaves","sourceId":"xml","type":"bar","encodings":{"x":{"field":"group"},"y":{"field":"count"}},"options":{"orientation":"horizontal","showValueLabels":true,"showLegend":false}}]
|
||||
},
|
||||
"snapshot": {"version":1,"status":"ready","generatedAt":"2026-09-09T11:30:00Z","datasets":{"xmlLeaves":[
|
||||
{"group":"Textwerte übernommen","count":664,"state":"retained","totalContextLeaves":758},
|
||||
{"group":"Explizit leere Elemente fehlen","count":70,"state":"missing","totalContextLeaves":758},
|
||||
{"group":"Berechtigungsattribute fehlen","count":16,"state":"missing","totalContextLeaves":758},
|
||||
{"group":"Verteilungs-/Graphattribute fehlen","count":8,"state":"missing","totalContextLeaves":758}
|
||||
]}},
|
||||
"sources": []
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# PurpleKnight in ASSESSOR – Importprüfung
|
||||
|
||||
## Verifizierte Umsetzung vom 09.09.2026
|
||||
|
||||
Die beauftragten Importfehler sind behoben. Der ursprüngliche Auditstand bleibt darunter als Historie erhalten.
|
||||
|
||||
- Profil **1.3.0**, HTML-Extractor **1.2.0**, Summary-Extractor **1.1.0**; bestehende Mapping-/Normalisierungsverträge bleiben kompatibel.
|
||||
- XLSX liefert nun **119 kanonische Ergebnisse**, 22 Kontextbeobachtungen und alle 20 verknüpften Detailtabellen mit 46 Zeilen. Die Summary verdrängt keine Indikatorentabelle mehr.
|
||||
- HTML liefert **119 Ergebnisse**, **45 Kontextbeobachtungen**, einen Reportabschnitt und alle 20 Detailtabellen mit 46 Zeilen. Bekannte Summary-Kennzahlen und unbekannte Originalmetadaten bleiben erhalten. Sämtliche 213 geprüften Detailzellen, einschließlich Sekunden, stimmen exakt mit der Quelle überein; 4.743 Originalfeldwerte wurden gegen ihre nativen Quellpfade geprüft. Abgeleitete Status-, Kategorie- und Laufzeitwerte referenzieren ihre realen Eingabeobjekte.
|
||||
- Die Haupt-CSV wird anhand der Kombination aus nativer Namensstruktur und Schema ohne zusätzliche Werkzeugbestätigung erkannt. Der CSV-Gesamtsatz liefert weiterhin 119 Ergebnisse, 22 Kontextbeobachtungen sowie 19 eindeutig verknüpfte Detaildateien mit 45 Zeilen. Dateiname allein erzwingt kein Profil.
|
||||
- Beschädigte Payloads, fehlende IDs, konkurrierende Zuweisungen und angekündigte, aber unlesbare Ergebnisse scheitern explizit. Wiederholte IDs, fehlende Kategorieverweise und Summen-/Detailabweichungen erhalten Diagnosen; vorhandene Ergebnisse werden nicht verworfen. Native Null-Details ohne Ergebnisse und deklarierte Leerberichte bleiben gültig.
|
||||
- **25 neue synthetische Regressionen. `npm test`: 1.122 bestanden, acht optionale PostgreSQL-Tests übersprungen. Sicherheits-/Designcheck und vollständiger Client-/Serverbuild erfolgreich.** Alle 24 Dateien wurden rein lesend mit beiden Runtimes und dem lokalen Parse-Endpunkt abgeglichen; der abschließende Drei-Format-Check am finalen Backend bestätigt Profil und Ergebniszahlen. Repositoryrevision unverändert.
|
||||
|
||||
AGENTS.md, Sicherheitsvorgaben, Importgrenzen und Umsetzungsplan enthalten die Erkenntnisse. Die temporären Audit-Skripte wurden entfernt. Der kanonische PostgreSQL-Datastore bleibt unverändert; App und aktualisiertes Backend laufen lokal. Kein Reimport, keine Datenbankmigration. **Bereits gespeicherte PurpleKnight-Importe benötigen einen ausdrücklichen Reimport**, um den neuen Erfassungsstand zu erhalten. Die separate AD-Domänen-CSV bleibt als Rohquelle erhalten; die typisierte HTML-Domänenliste ist Teil des neuen Kontexts. Phase 13C bleibt insgesamt in Arbeit.
|
||||
|
||||
## Ursprünglicher Prüfstand
|
||||
|
||||
Stand: 09.09.2026 · ASSESSOR 2.5.5 · Prüfung, keine Implementierung
|
||||
|
||||
## Ergebnis
|
||||
|
||||
PurpleKnight wird noch nicht vollständig verarbeitet. Der schwerste Fehler betrifft das vollständige Excel-Workbook: Seine 119 Indikatorergebnisse ergeben **0 kanonische Findings**. HTML und der bestätigte CSV-Gesamtimport liefern dagegen jeweils sämtliche 119 Indikatorergebnisse und die zugehörigen Ergebnistabellen. HTML verliert jedoch Berichtskontext und Zeitstempelpräzision; außerdem fehlen Vollständigkeitsprüfungen.
|
||||
|
||||
„119 Ergebnisse“ umfasst bestandene und nicht ausgeführte beziehungsweise nicht relevante Prüfungen, nicht 119 Sicherheitsprobleme.
|
||||
|
||||
## Prüfumfang und Methode
|
||||
|
||||
Geprüft wurden ausschließlich die 24 vom Nutzer freigegebenen Dummy-Dateien: 22 CSV-Dateien, ein HTML-Bericht und ein XLSX-Workbook. Der Auftrag gehört zu Phase 13A/13C des Implementierungsplans: Importpipeline, Profilverarbeitung und vollständiger Umgebungskontext.
|
||||
|
||||
- Unabhängige Kontrolllesung mit Python-CSV-Reader und direkter OOXML-Zelllesung. Der Spreadsheet-Prüfworkflow wurde rein lesend angewendet; das Workbook blieb unverändert. Die direkte OOXML-Lesung umging eine nicht unterstützte Layout-Eigenschaft im unabhängigen Excel-Leser, nicht einen ASSESSOR-Parserfehler.
|
||||
- HTML-Skriptblöcke ausschließlich als Text und eingebettetes JSON gelesen, niemals ausgeführt.
|
||||
- Alle Dateien durch Browser- und Node-Importadapter, Vorschau, erforderliche Mappingbestätigung, kanonisches Modell, Modellvalidierung und Work-Dataset-Projektion geprüft.
|
||||
- Zusätzlich alle 24 Dateien gegen den laufenden lokalen Backend-Parse-Endpunkt geprüft: jeweils HTTP 200 und identische Extraktion; Repository-Revision vorher/nachher unverändert.
|
||||
- Synthetische Gegenproben für Workbook-Extractor-Auswahl und beschädigte beziehungsweise widersprüchliche HTML-Berichte.
|
||||
- 47 vorhandene fokussierte Tests in neun Testdateien bestanden.
|
||||
|
||||
Kein Import-/Reimport-Abschluss, keine Datenbankmigration, kein Zugriff auf fremde Kundendaten und keine Produktionscodeänderung. Bestehende persistierte PurpleKnight-Bestände und eine befüllte Oberfläche wurden nicht inhaltlich untersucht. Vollständiger Build und Gesamttests waren nicht Gegenstand dieser reinen Prüfung.
|
||||
|
||||
## Abgleich nach Format
|
||||
|
||||
| Quelle | Native Indikatorergebnisse | Kanonische Ergebnisse | Detailverknüpfungen | Berichtskontext |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
| CSV-Gesamtsatz, 08:23 | 119 | 119 nach Bestätigung | 19/19 eindeutig, 45 Detailzeilen | 22 Beobachtungen, ein Summary-Abschnitt |
|
||||
| XLSX, 09:14 | 119 | **0** | Keine Work-Projektion aus diesem Einzelimport | 22 Beobachtungen, ein Summary-Abschnitt |
|
||||
| HTML, 09:14 | 119 | 119 | 20/20 eindeutig, 46 Detailzeilen | **0 Beobachtungen, 0 Berichtabschnitte** |
|
||||
|
||||
Die CSV-Dateien stammen aus einem früheren Lauf als HTML/XLSX. Ihre Abweichung ist kein Importfehler:
|
||||
|
||||
| Lauf | Bestanden | IOE gefunden | Nicht relevant | Nicht ausgewählt | Nativer Posture Score |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| CSV 08:23 | 96 | 19 | 3 | 1 | 65 |
|
||||
| HTML/XLSX 09:14 | 95 | 20 | 4 | 0 | 59 |
|
||||
|
||||
Alle 119 nativen Indikator-IDs stimmen zwischen den Läufen überein; zwei Statuswerte haben sich geändert. Die CSV-Hauptdatei erhält sämtliche 26 Spaltenwerte für alle 119 Zeilen. Die 19 CSV-Detaildateien erhalten sämtliche geprüften Zellwerte.
|
||||
|
||||
## Priorisierte Befunde
|
||||
|
||||
### 1. P1 – Summary-Extractor verdrängt Excel-Indikatoren
|
||||
|
||||
Das Workbook enthält 23 Blätter, darunter „Assessment summary“, „Indicators results“ mit 119 Indikatoren sowie 20 Detailblätter. Die Erkennung wählt die Indikatorentabelle korrekt aus. Anschließend gewinnt jedoch der Summary-Extractor für das gesamte Dokument und liefert absichtlich keine Finding-Kandidaten. Die kanonische Validierung akzeptiert das Ergebnis ohne Warnung. Quelltabellen bleiben erhalten, aber die 119 Indikatorergebnisse werden nicht als Findings aufgebaut; die Work-Projektion des alleinigen Workbooks bleibt leer.
|
||||
|
||||
Ursache: [Summary-Erkennung und Extraktion](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/purple-knight-summary-source-extractor.ts:170>) in Verbindung mit der [Auswahl genau eines Extractors](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/source-extractor-registry.ts:5>).
|
||||
|
||||
Fix: Summary-Kontext, Indikatorentabelle und Detailblätter gemeinsam über die generische Pipeline verarbeiten. Den Summary-Extractor nicht einfach abschalten, sonst geht der bereits verfügbare Kontext verloren. Synthetisch mit einem Summary- und einem Indikatorenblatt reproduziert.
|
||||
|
||||
Abnahme: 119 Ergebnisse, 20 eindeutig verknüpfte Detailtabellen mit 46 Zeilen und weiterhin 22 Summary-Beobachtungen aus diesem Workbook; keine Summary-Zeile als Finding.
|
||||
|
||||
### 2. P1 – HTML-Berichtskontext wird nicht übernommen
|
||||
|
||||
Im eingebetteten HTML-Modell stehen unter anderem Posture Score 59, Gesamtzahl 119, 95 bestandene Prüfungen, 20 IOEs, vier nicht relevante Prüfungen, sechs bewertete Kategorien, Werkzeugversion, Erstellungszeit und Umgebungskontext. Der Extractor übernimmt lediglich Indikator-Kandidaten und Detailtabellen. Das kanonische Modell enthält weder Umgebungsbeobachtungen noch Berichtabschnitte.
|
||||
|
||||
Ursache: [HTML-Extraktion](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/purple-knight-html-source-extractor.ts:191>).
|
||||
|
||||
Fix: Native HTML-Summary und Kategorien auf dieselben Kontextverträge wie CSV/XLSX abbilden. Native Scores separat und mit Herkunft führen, nicht mit vergleichbaren ASSESSOR-Scores verwechseln. Fehlende Werte bleiben unbekannt, nicht 0 beziehungsweise 0 %.
|
||||
|
||||
Abnahme: Inhaltlich äquivalente HTML-/XLSX-Summary liefert dieselben normalisierten Kennzahlen bei erhaltener formatabhängiger Provenienz.
|
||||
|
||||
### 3. P1 – HTML-Teilimporte werden nicht als unvollständig erkannt
|
||||
|
||||
Synthetische Gegenproben durch Extraktion und kanonisches Modell:
|
||||
|
||||
| Gegenprobe | Beobachtetes Verhalten |
|
||||
| --- | --- |
|
||||
| Deklarierte Gesamtzahl 99, tatsächlich zwei Indikatoren | Zwei Ergebnisse; Extraktion „complete“; keine Abweichungsdiagnose |
|
||||
| Ein fehlender `ResIndicator`-Block | Nur ein statt zwei Ergebnissen; „complete“ |
|
||||
| Ein beschädigter JSON-Indikator | Nur ein statt zwei Ergebnissen; „complete“ |
|
||||
| Zwei Indikatoren mit gleicher `ShortName` | Zwei Vorkommen; keine Identitätskonfliktdiagnose |
|
||||
|
||||
Die generische Warnung über entfernten aktiven HTML-Inhalt ist vorhanden, erklärt aber keinen dieser Vollständigkeitsfehler. Bei den freigegebenen 119 HTML-Indikatoren wurde kein solcher Ausfall festgestellt; hier geht es um eine nachgewiesene Schutzlücke.
|
||||
|
||||
Ursache: [Payload-Lesung](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/purple-knight-html-source-extractor.ts:28>) überspringt nicht lesbare Objekte und gleicht das Ergebnis nicht mit der nativen Berichtsbilanz ab.
|
||||
|
||||
Fix: Deklarierte, gelesene und kanonisch übernommene Ergebnisse sowie Detailanzahlen abgleichen; beschädigte Objekte, fehlende IDs und Identitätskonflikte explizit melden. Widersprüchliche Identitäten nicht pauschal zusammenlegen. Fehler je nach Ausmaß als partiellen oder fehlgeschlagenen Import ausweisen.
|
||||
|
||||
### 4. P2 – HTML-Originaltreue und Feldherkunft unvollständig
|
||||
|
||||
- Von 213 geprüften Detailzellen wurden 20 Zeitstempel verändert: Ihre vorhandenen, nicht nullwertigen Sekunden verschwinden durch die Normalisierung auf Minuten. Alle 46 Zeilen bleiben erhalten, aber diese Werte sind nicht mehr exakt. Die [Datumsumwandlung](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/purple-knight-html-source-extractor.ts:116>) muss den Originalwert neben einem separat normalisierten Wert erhalten.
|
||||
- Bei allen 119 Indikatoren zeigen die Feldpfade für die sechs geprüften Felder `ShortName`, `Name`, `Description`, `Severity`, `Score` und `Remediation` auf die falsche Objektebene. Beispielsweise fehlt `ResIndicator` beziehungsweise `ExecutionResult` im Pfad. Die Werte selbst wurden gelesen; ihre [Feldprovenienz](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/purple-knight-html-source-extractor.ts:57>) ist jedoch unzutreffend. Abgeleitete Werte brauchen ebenfalls explizite Ableitungsherkunft.
|
||||
|
||||
Die Originaldatei wird im kanonischen Import [nicht separat aufbewahrt](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/importers/canonical-import.ts:167>). Deshalb ist eine spätere Wiederherstellung der verlorenen Zeitpräzision aus dem kanonischen Modell allein nicht gesichert.
|
||||
|
||||
Textabweichungen wurden nicht pauschal als Verlust gezählt: 95 Unterschiede in „Likelihood of compromise“ und sechs in „Description“ betreffen die Darstellung. Sieben Remediation-Werte unterscheiden sich durch Whitespace oder HTML-zu-Klartext-Umwandlung; zwei enthalten Links, deren Ziele im Klartext fehlen. Die ursprünglichen Remediation-HTML-Werte bleiben jedoch als Originalfelder erhalten. Dies ist kein nachgewiesener vollständiger Verlust dieser Maßnahmen oder Links aus dem kanonischen Modell.
|
||||
|
||||
### 5. P2 – CSV-Erkennung benötigt zusätzliche Bestätigung
|
||||
|
||||
Die Haupt-CSV wird als PurpleKnight vorgeschlagen, erreicht aber nur 0,54 Konfidenz und verwendet bis zur Bestätigung das generische Profil. Nach der regulären Bestätigung ist das Profil korrekt `purple-knight`; es handelt sich nicht um einen dauerhaft generischen oder fehlerhaften Import.
|
||||
|
||||
Fixoption: Die Kombination aus typischer CSV-Namensstruktur und charakteristischen Spalten besser erkennen, ohne Dateinamen allein zu vertrauen. Die Sicherheitsgrenze für unbekannte oder mehrdeutige Dateien beibehalten. [Profilregeln](<C:/Users/milu/OneDrive - Exxeta/Dokumente/VS CODE/ASSESSOR-2-5/src/profiles/purple-knight/purple-knight-definition.ts:28>).
|
||||
|
||||
Die separate AD-Domänen-Datei bleibt als Quelltabelle erhalten, erzeugt aber keine typisierten Domänenbeobachtungen. Ob ihre Einzelangaben zusätzlich als Umfeldkontext benötigt werden, sollte mit der Kontextabbildung aus Punkt 2 vereinheitlicht werden; dies ist kein Verlust ihrer Rohdaten.
|
||||
|
||||
## Empfohlene Umsetzung
|
||||
|
||||
Zuerst Punkte 1–3 beheben, anschließend Originaltreue und Provenienz aus Punkt 4 absichern. Die CSV-Erkennungsverbesserung ist nachrangig.
|
||||
|
||||
Verbindliche Regressionen sollten vollständige Multi-Sheet-Workbooks, semantische Formatparität innerhalb desselben Scan-Laufs, vollständige Ergebnistabellen-Verknüpfung, exakte Originalwerte, auflösbare Feldpfade und explizite Teilimportdiagnosen abdecken. Prüfungen gegen unabhängige native Summen dürfen nicht nur die Anzahl der bereits extrahierten Kandidaten bestätigen. CSV- und HTML/XLSX-Läufe verschiedener Zeitpunkte müssen getrennt bleiben.
|
||||
|
||||
Dieser Bericht dokumentiert einen Prüfstand. Die genannten Fehler sind noch nicht behoben; keine Phase oder Abnahme wurde als abgeschlossen markiert. Temporäre Audit-Skripte wurden nach der Prüfung entfernt.
|
||||
|
|
@ -249,6 +249,7 @@ function unifiedQueryFrom(url: URL): UnifiedResultQuery {
|
|||
outcome: many('outcome') as UnifiedResultQuery['outcome'],
|
||||
technicalExecutionState: many('technicalExecutionState') as UnifiedResultQuery['technicalExecutionState'],
|
||||
resultType: many('resultType') as UnifiedResultQuery['resultType'],
|
||||
policyClassification: many('policyClassification') as UnifiedResultQuery['policyClassification'],
|
||||
isIoe: booleanValue('isIoe'),
|
||||
deviation: booleanValue('deviation'),
|
||||
mappingRuleVersion: many('mappingRuleVersion'),
|
||||
|
|
@ -613,7 +614,8 @@ export function createAssessorHttpServer(options: AssessorHttpServerOptions): Se
|
|||
scanRunId: committed.scanRunId,
|
||||
replacedScanRunId: body.replacesScanRunId,
|
||||
importedFiles: committed.importedFiles,
|
||||
importedOccurrences: committed.importedOccurrences
|
||||
importedOccurrences: committed.importedOccurrences,
|
||||
reimportReviewTransfer: committed.reimportReviewTransfer
|
||||
};
|
||||
status = 200; sendJson(response, status, result, requestId); return;
|
||||
}
|
||||
|
|
@ -829,8 +831,8 @@ export function createAssessorHttpServer(options: AssessorHttpServerOptions): Se
|
|||
if (!body || typeof body.replacementScanRunId !== 'string' || !body.replacementScanRunId.trim()) {
|
||||
throw new HttpError(400, 'replacement_scan_required', 'Der neu importierte Ersatzscan ist erforderlich.');
|
||||
}
|
||||
await repository.adoptScanRunReplacement(segments[1], segments[3], body.replacementScanRunId, now().toISOString());
|
||||
status = 200; sendJson(response, status, { repositoryRevision: repository.revision }, requestId); return;
|
||||
const reimportReviewTransfer = await repository.adoptScanRunReplacement(segments[1], segments[3], body.replacementScanRunId, now().toISOString());
|
||||
status = 200; sendJson(response, status, { repositoryRevision: repository.revision, reimportReviewTransfer }, requestId); return;
|
||||
}
|
||||
if (segments[0] === 'projects' && segments[2] === 'scan-runs' && segments.length === 4 && method === 'DELETE') {
|
||||
const confirmation = await readJson<{ projectId: string; scanRunId: string; cascade: true }>(request, maximumBodyBytes);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { BackendArtifactStore } from '../artifacts/backend-artifact-store';
|
|||
import { GPO_BACKUP_ANALYZER_VERSION } from './gpo-backup-submission-service';
|
||||
import type { GpoBackupWorkerExecutor } from './gpo-inventory-executor';
|
||||
|
||||
export const GPO_BACKUP_JOB_PROCESSOR_VERSION = '2.2.2';
|
||||
export const GPO_BACKUP_JOB_PROCESSOR_VERSION = '2.2.3';
|
||||
|
||||
export interface GpoBackupJobProcessorOptions {
|
||||
leaseDurationMs?: number;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/pe
|
|||
import type { BackendArtifactStore } from '../artifacts/backend-artifact-store';
|
||||
import type { GpoInventoryExecutor } from './gpo-inventory-executor';
|
||||
|
||||
export const GPO_BACKUP_ANALYZER_VERSION = 'gpo-backup-policy-analysis:2.2.2';
|
||||
export const GPO_BACKUP_ANALYZER_VERSION = 'gpo-backup-policy-analysis:2.2.3';
|
||||
|
||||
export interface SubmitGpoBackupRequest {
|
||||
projectId: string;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ function uniqueTimestamp(values: string[], diagnostics: Set<string>): string | u
|
|||
}
|
||||
export function parseGpoBackupMetadataXml(
|
||||
file: VerifiedZipFile,
|
||||
resourceOverrides?: Partial<ImportResourcePolicy>
|
||||
resourceOverrides?: Partial<ImportResourcePolicy>,
|
||||
includeTemplateNames = false
|
||||
): GpoBackupMetadataDocument {
|
||||
const text = decodeXml(file.bytes);
|
||||
const policy = resolveImportResourcePolicy({
|
||||
|
|
@ -116,7 +117,9 @@ export function parseGpoBackupMetadataXml(
|
|||
}
|
||||
const path = [...stack.map(item => item.name), name].join('/');
|
||||
const core = 'grouppolicybackupscheme/grouppolicyobject/grouppolicycoresettings/';
|
||||
const field = path === core + 'id' || path === 'gpo/identifier/identifier' ? 'gpoid'
|
||||
const templateName = includeTemplateNames && [core + 'displayname', 'gpo/name',
|
||||
'backupinst/gpodisplayname', 'backupinst/gponame', 'backupinst/displayname'].includes(path);
|
||||
const field = templateName ? 'templatename' : path === core + 'id' || path === 'gpo/identifier/identifier' ? 'gpoid'
|
||||
: path === core + 'domain' || path === 'gpo/identifier/domain' ? 'gpodomain'
|
||||
: name === 'id' && parent?.name === 'backupinst' ? 'backupid'
|
||||
: ['gpoid', 'gpoguid', 'backupid', 'gpodomain', 'gpodomainguid', 'domainname', 'backuptime'].includes(name) ? name : undefined;
|
||||
|
|
@ -153,7 +156,8 @@ export function parseGpoBackupMetadataXml(
|
|||
const invalid = [...diagnostics].some(code => code.endsWith('-invalid') || code.endsWith('-conflict'));
|
||||
return {
|
||||
version: 1,
|
||||
parser,
|
||||
parser: includeTemplateNames ? { ...parser, version: '1.3.0' } : parser,
|
||||
...(includeTemplateNames ? { templateNames: [...new Set(values.get('templatename') ?? [])].sort() } : {}),
|
||||
sourcePath: file.entry.relativePath,
|
||||
sourceSha256: file.entry.sha256,
|
||||
status: invalid ? 'invalid' : gpoId && backupId && domainKey && backupCreatedAt ? 'complete' : 'partial',
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
type StoredPolicyEvaluation
|
||||
} from '../../src/domain/policy-evaluation';
|
||||
import { comparePolicyValues } from '../../src/evaluation/policy-comparator';
|
||||
import { selectBaselineTemplates } from '../../src/evaluation/baseline-template-context';
|
||||
import {
|
||||
RELEASED_POLICY_EVALUATION_RULE_CATALOG,
|
||||
policyEvaluationRuleCatalogValidationErrors,
|
||||
|
|
@ -61,10 +62,15 @@ function evaluateBaselineIdentity(
|
|||
runId: string,
|
||||
analysis: StoredMicrosoftSecurityBaselineAnalysis,
|
||||
baseline: MicrosoftSecurityBaselineRecord,
|
||||
settings: MicrosoftSecurityBaselineSetting[],
|
||||
state: AggregatedPolicyState | undefined
|
||||
candidates: MicrosoftSecurityBaselineSetting[],
|
||||
state: AggregatedPolicyState | undefined,
|
||||
profile: EnvironmentProfile
|
||||
): { result: PolicyEvaluationResult; evidence: PolicyEvaluationEvidence[] } {
|
||||
const diagnostics = new Set<string>();
|
||||
const selection = selectBaselineTemplates(candidates, profile);
|
||||
const settings = selection.settings;
|
||||
if (selection.status === 'selected') diagnostics.add('policy-evaluation-template-context-selected');
|
||||
if (selection.status === 'no-match') diagnostics.add('policy-evaluation-template-context-no-match');
|
||||
const identity = settings[0].identity.status === 'resolved' ? settings[0].identity.identity : undefined;
|
||||
if (!identity) throw new RepositoryValidationError('Eine Baselinegruppe besitzt keine aufgeloeste technische Identitaet.');
|
||||
if (analysis.analysis.status !== 'complete' || analysis.analysis.admxCatalog.status !== 'complete') {
|
||||
|
|
@ -72,6 +78,9 @@ function evaluateBaselineIdentity(
|
|||
}
|
||||
const recommendationSignatures = new Set(settings.map(setting => canonicalJson(setting.observation.values[0])));
|
||||
if (recommendationSignatures.size > 1) diagnostics.add('policy-evaluation-baseline-consensus-conflict');
|
||||
if (recommendationSignatures.size > 1 && selection.status === 'unresolved') {
|
||||
diagnostics.add('policy-evaluation-template-context-required');
|
||||
}
|
||||
const customerSignatures = new Set(state?.values.map(value => canonicalJson(value)) ?? []);
|
||||
if (state && customerSignatures.size !== 1) diagnostics.add('policy-evaluation-customer-value-ambiguous');
|
||||
const customerState: PolicyEvaluationResult['customerState'] = !state || state.status === 'deleted'
|
||||
|
|
@ -87,18 +96,18 @@ function evaluateBaselineIdentity(
|
|||
baseline: undefined,
|
||||
diagnosticCodes: []
|
||||
};
|
||||
if (recommendationSignatures.size === 1 && customerState === 'missing') {
|
||||
if (selection.status !== 'no-match' && recommendationSignatures.size === 1 && customerState === 'missing') {
|
||||
compared = comparePolicyValues(undefined, settings[0].observation.values[0], { kind: 'equality' });
|
||||
if (compared.baseline !== undefined) {
|
||||
compared = { ...compared, relation: 'different', diagnosticCodes: [] };
|
||||
} else {
|
||||
compared.diagnosticCodes.forEach(code => diagnostics.add(code));
|
||||
}
|
||||
} else if (recommendationSignatures.size === 1 && customerState === 'configured' && customerSignatures.size === 1 && state) {
|
||||
} else if (selection.status !== 'no-match' && recommendationSignatures.size === 1 && customerState === 'configured' && customerSignatures.size === 1 && state) {
|
||||
compared = comparePolicyValues(state.values[0], settings[0].observation.values[0], { kind: 'equality' });
|
||||
compared.diagnosticCodes.forEach(code => diagnostics.add(code));
|
||||
}
|
||||
const review = recommendationSignatures.size !== 1 || ['conflicted', 'indeterminate'].includes(customerState)
|
||||
const review = selection.status === 'no-match' || recommendationSignatures.size !== 1 || ['conflicted', 'indeterminate'].includes(customerState)
|
||||
|| compared.relation === 'incomparable';
|
||||
const classification: PolicyEvaluationResult['classification'] = review
|
||||
? 'review_required'
|
||||
|
|
@ -122,11 +131,13 @@ function evaluateBaselineIdentity(
|
|||
ruleIds: [],
|
||||
dependencyRuleIds: [],
|
||||
riskRuleIds: [],
|
||||
contextIds: sortedUnique(state?.applicableContextIds ?? []),
|
||||
contextIds: sortedUnique([...selection.contextIds, ...(state?.applicableContextIds ?? [])]),
|
||||
observationIds: customerState === 'missing' ? [] : sortedUnique(state?.observationIds ?? []),
|
||||
backupInstanceIds: customerState === 'missing' ? [] : sortedUnique(state?.backupInstanceIds ?? []),
|
||||
baselineAnalysisIds,
|
||||
baselineSettingIds,
|
||||
baselineTemplateSelection: { status: selection.status, contextIds: selection.contextIds,
|
||||
candidateSettingIds: sortedUnique(candidates.map(setting => setting.id)) },
|
||||
findingEligibility: 'ineligible',
|
||||
effectiveState: 'unknown',
|
||||
diagnosticCodes: [...diagnostics].sort()
|
||||
|
|
@ -136,7 +147,7 @@ function evaluateBaselineIdentity(
|
|||
evidence: evidenceFor(resultId, [
|
||||
{ kind: 'customer-observation', sourceIds: result.observationIds },
|
||||
{ kind: 'gpo-backup', sourceIds: result.backupInstanceIds },
|
||||
{ kind: 'baseline-setting', sourceIds: baselineSettingIds },
|
||||
{ kind: 'baseline-setting', sourceIds: result.baselineTemplateSelection!.candidateSettingIds },
|
||||
{ kind: 'baseline-analysis', sourceIds: baselineAnalysisIds },
|
||||
{ kind: 'environment-context', sourceIds: result.contextIds }
|
||||
])
|
||||
|
|
@ -172,7 +183,7 @@ export function buildPolicyEvaluation(input: EvaluationBuildInput): StoredPolicy
|
|||
grouped.set(id, [...(grouped.get(id) ?? []), setting]);
|
||||
}
|
||||
return [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([identityId, settings]) => evaluateBaselineIdentity(runId, analysis, baseline, settings, statesByIdentity.get(identityId)));
|
||||
.map(([identityId, settings]) => evaluateBaselineIdentity(runId, analysis, baseline, settings, statesByIdentity.get(identityId), input.profile));
|
||||
});
|
||||
const results = built.map(item => item.result).sort((left, right) => left.id.localeCompare(right.id));
|
||||
const evidence = built.flatMap(item => item.evidence).sort((left, right) => left.id.localeCompare(right.id));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { parseRegistryPolicyFile, reduceRegistryPolicy } from './registry-policy
|
|||
import { parseSecurityTemplateInf, reduceSecurityTemplate } from './security-template-parser';
|
||||
import { parseAdvancedAuditCsv, reduceAdvancedAuditPolicy } from './advanced-audit-parser';
|
||||
import { resolvePolicyTarget } from './policy-target-resolver';
|
||||
import { baselineTemplateContext } from '../../src/evaluation/baseline-template-context';
|
||||
|
||||
export interface MicrosoftSecurityBaselineAnalysisOptions {
|
||||
baselineId: string;
|
||||
|
|
@ -39,6 +40,7 @@ function stableId(prefix: string, parts: string[]): string {
|
|||
|
||||
function selectedBaselineEntry(path: string): boolean {
|
||||
return /(?:^|\/)Backup\.xml$/i.test(path)
|
||||
|| /(?:^|\/)(?:bkupInfo|GPReport)\.xml$/i.test(path)
|
||||
|| /(?:^|\/)Registry\.pol$/i.test(path)
|
||||
|| /\.(?:admx|adml)$/i.test(path)
|
||||
|| ['security-template', 'advanced-audit-policy'].includes(classifyGpoArtifactPath(path).role);
|
||||
|
|
@ -74,7 +76,7 @@ export function analyzeMicrosoftSecurityBaselinePackage(
|
|||
for (const file of metadataFiles) {
|
||||
const root = parentPath(file.entry.relativePath);
|
||||
if (!roots.includes(root)) continue;
|
||||
const document = parseGpoBackupMetadataXml(file, options.resourcePolicy);
|
||||
const document = parseGpoBackupMetadataXml(file, options.resourcePolicy, true);
|
||||
metadataByRoot.set(root, [...(metadataByRoot.get(root) ?? []), document]);
|
||||
document.diagnosticCodes.forEach(code => diagnostics.add(code));
|
||||
}
|
||||
|
|
@ -84,10 +86,20 @@ export function analyzeMicrosoftSecurityBaselinePackage(
|
|||
baselineId: options.baselineId,
|
||||
packageSha256: archive.containerSha256
|
||||
});
|
||||
const analysisId = stableId('microsoft-baseline-analysis', [options.baselineId, MICROSOFT_BASELINE_SETTINGS_ANALYZER.version]);
|
||||
// Each immutable analysis owns a catalog row, even when its ADMX contents are unchanged.
|
||||
admxCatalog.id = stableId('admx-catalog', [analysisId, admxCatalog.id]);
|
||||
admxCatalog.diagnosticCodes.forEach(code => diagnostics.add(code));
|
||||
const settings: MicrosoftSecurityBaselineSetting[] = [];
|
||||
const parsedMetadata = new Map([...metadataByRoot.values()].flat().map(document => [document.sourcePath, document]));
|
||||
let partial = admxCatalog.status === 'partial';
|
||||
for (const root of [...new Set(roots)].sort()) {
|
||||
const names = archive.files.filter(file => parentPath(file.entry.relativePath) === root
|
||||
&& /(?:^|\/)(?:Backup|bkupInfo|GPReport)\.xml$/i.test(file.entry.relativePath))
|
||||
.flatMap(file => ((parsedMetadata.get(file.entry.relativePath)
|
||||
?? parseGpoBackupMetadataXml(file, options.resourcePolicy, true)).templateNames ?? [])
|
||||
.map(value => ({ value, sourcePath: file.entry.relativePath, sourceSha256: file.entry.sha256 })));
|
||||
const template = { names, context: baselineTemplateContext(names.map(item => item.value)) };
|
||||
const aggregate = aggregateGpoBackupMetadata(metadataByRoot.get(root) ?? []);
|
||||
aggregate.diagnosticCodes.forEach(code => diagnostics.add(code));
|
||||
if (aggregate.status !== 'complete' || !aggregate.backupId) partial = true;
|
||||
|
|
@ -131,6 +143,7 @@ export function analyzeMicrosoftSecurityBaselinePackage(
|
|||
]),
|
||||
baselineId: options.baselineId,
|
||||
backupId,
|
||||
template,
|
||||
observation: {
|
||||
sourcePath: observation.sourcePath,
|
||||
sourceSha256: observation.sourceSha256,
|
||||
|
|
@ -154,7 +167,7 @@ export function analyzeMicrosoftSecurityBaselinePackage(
|
|||
}
|
||||
const analysis: MicrosoftSecurityBaselineAnalysis = {
|
||||
version: 1,
|
||||
id: stableId('microsoft-baseline-analysis', [options.baselineId, MICROSOFT_BASELINE_SETTINGS_ANALYZER.version]),
|
||||
id: analysisId,
|
||||
baselineId: options.baselineId,
|
||||
packageSha256: archive.containerSha256,
|
||||
analyzer: MICROSOFT_BASELINE_SETTINGS_ANALYZER,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { resolveImportResourcePolicy, validateDecodedTextResources } from '../..
|
|||
import { ImportError } from '../../src/shared/errors';
|
||||
import type { VerifiedZipFile } from './zip-inventory';
|
||||
|
||||
export const SECURITY_TEMPLATE_PARSER = { id: 'gpo-security-template-inf', version: '1.0.0' } as const;
|
||||
export const SECURITY_TEMPLATE_PARSER = { id: 'gpo-security-template-inf', version: '1.1.0' } as const;
|
||||
export const SECURITY_TEMPLATE_REDUCER = { id: 'gpo-security-template-reducer', version: '1.0.0' } as const;
|
||||
|
||||
const supportedSections = new Set(['system access', 'kerberos policy', 'privilege rights', 'registry values']);
|
||||
|
|
@ -89,21 +89,26 @@ export function parseSecurityTemplateInf(
|
|||
else if (!supportedSections.has(section) && !metadataSections.has(section)) diagnostics.add('gpo-security-template-section-unsupported');
|
||||
continue;
|
||||
}
|
||||
// A damaged section boundary makes the attribution of following entries unsafe.
|
||||
if (trimmed.startsWith('[')) {
|
||||
diagnostics.add('gpo-security-template-section-invalid');
|
||||
invalid = true;
|
||||
section = undefined;
|
||||
continue;
|
||||
}
|
||||
if (!section) { diagnostics.add('gpo-security-template-entry-outside-section'); invalid = true; continue; }
|
||||
// Other INF sections have their own grammars, e.g. comma-separated service entries.
|
||||
if (metadataSections.has(section) || !supportedSections.has(section)) continue;
|
||||
const separator = rawLine.indexOf('=');
|
||||
if (separator < 1) { diagnostics.add('gpo-security-template-entry-invalid'); invalid = true; continue; }
|
||||
if (separator < 1) { diagnostics.add('gpo-security-template-entry-invalid'); continue; }
|
||||
const key = rawLine.slice(0, separator).trim();
|
||||
const rawValue = rawLine.slice(separator + 1).trim();
|
||||
if (!key || key.length > 1_024 || rawValue.length > maximumFieldCharacters) {
|
||||
diagnostics.add('gpo-security-template-entry-invalid');
|
||||
invalid = true;
|
||||
continue;
|
||||
}
|
||||
if (metadataSections.has(section)) continue;
|
||||
if (!supportedSections.has(section)) continue;
|
||||
if (section === 'registry values' && !/^\s*\d+\s*,/.test(rawValue)) {
|
||||
diagnostics.add('gpo-security-template-registry-value-invalid');
|
||||
invalid = true;
|
||||
continue;
|
||||
}
|
||||
instructions.push({ version: 1, section, key, rawValue, line });
|
||||
|
|
@ -114,7 +119,7 @@ export function parseSecurityTemplateInf(
|
|||
sourcePath: file.entry.relativePath,
|
||||
sourceSha256: file.entry.sha256,
|
||||
scope,
|
||||
status: invalid ? 'invalid' : diagnostics.has('gpo-security-template-section-unsupported') ? 'partial' : 'complete',
|
||||
status: invalid ? 'invalid' : diagnostics.size ? 'partial' : 'complete',
|
||||
instructions: invalid ? [] : instructions,
|
||||
diagnosticCodes: [...diagnostics].sort()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { JSDOM } from 'jsdom';
|
|||
import type {
|
||||
DecodedHtmlDocument,
|
||||
DecodedXmlDocument,
|
||||
DecodedXmlRow,
|
||||
ImportRuntimeAdapters,
|
||||
MarkupDocumentAdapter,
|
||||
TextDecoderAdapter
|
||||
|
|
@ -13,14 +14,15 @@ import {
|
|||
shouldUseBoundedHtmlExtraction
|
||||
} from '../src/importers/html-extraction';
|
||||
|
||||
function walkXml(node: Element, path: string, rows: Array<{ path: string; attributes: string; text: string }>): void {
|
||||
function walkXml(node: Element, path: string, rows: DecodedXmlRow[]): void {
|
||||
const attributes = [...node.attributes].map(attribute => `${attribute.name}="${attribute.value}"`).join('; ');
|
||||
const attributeValues = Object.fromEntries([...node.attributes].map(attribute => [attribute.name, attribute.value]));
|
||||
const text = [...node.childNodes]
|
||||
.filter(child => child.nodeType === 3)
|
||||
.filter(child => child.nodeType === 3 || child.nodeType === 4)
|
||||
.map(child => child.textContent?.trim() ?? '')
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
rows.push({ path, attributes, text });
|
||||
rows.push({ path, attributes, attributeValues, text });
|
||||
[...node.children].forEach((child, index) => walkXml(child, `${path}/${child.nodeName}[${index + 1}]`, rows));
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ export const nodeMarkupDocumentAdapter: MarkupDocumentAdapter = {
|
|||
try {
|
||||
const documentNode = new JSDOM(text, { contentType: 'application/xml' }).window.document;
|
||||
const root = documentNode.documentElement;
|
||||
const rows: Array<{ path: string; attributes: string; text: string }> = [];
|
||||
const rows: DecodedXmlRow[] = [];
|
||||
walkXml(root, root.nodeName, rows);
|
||||
return { valid: true, rootName: root.nodeName, rows };
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -53,11 +53,13 @@ import {
|
|||
type UnifiedResultQuery,
|
||||
type UnifiedResultQueryResult
|
||||
} from '../../src/domain/unified-result';
|
||||
import { createPolicyConsistencyUnifiedResults } from '../../src/evaluation/policy-consistency-results';
|
||||
import { deriveExposureState, deriveIsIoe, deriveResultType } from '../../src/domain/result-contract-mapping';
|
||||
import { importArtifactValidationErrors, ingestionJobValidationErrors, processingRunValidationErrors } from '../../src/domain/ingestion-persistence';
|
||||
import { assertCanonicalAssessmentModel } from '../../src/domain/canonical-validation';
|
||||
import { processingScopeIssues } from '../../src/domain/processing-compatibility';
|
||||
import { compareReimportSourceHashes } from '../../src/domain/reimport-source';
|
||||
import { planReimportReviewTransfer, type ReimportReviewTransferPlan, type ReimportReviewTransferSummary } from '../../src/domain/reimport-review-transfer';
|
||||
import { canonicalJson } from '../../src/shared/canonical-json';
|
||||
import { validateLocalComparisonRules } from '../../src/comparison/local-mapping-rules';
|
||||
import type {
|
||||
|
|
@ -117,6 +119,18 @@ function dataRow<T>(result: QueryResult<DataRow<T>>): T | null {
|
|||
return result.rows[0] ? clone(result.rows[0].data) : null;
|
||||
}
|
||||
|
||||
async function persistTransferredReviews(client: PoolClient, reviews: readonly FindingReview[]): Promise<void> {
|
||||
for (const review of reviews) {
|
||||
const definitionId = review.target.kind === 'definition' ? review.target.definitionId : null;
|
||||
const occurrenceId = review.target.kind === 'occurrence' ? review.target.occurrenceId : null;
|
||||
await client.query(`
|
||||
INSERT INTO assessor_reviews (id, target_kind, definition_id, occurrence_id, revision, updated_at, data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`, [review.id, review.target.kind, definitionId, occurrenceId, review.revision, review.updatedAt, json(review)]);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistScannerContext(
|
||||
queryable: Queryable,
|
||||
sections: ScannerReportSection[],
|
||||
|
|
@ -717,6 +731,7 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
});
|
||||
const expectedRevision = this.cachedRevision;
|
||||
return this.mutate(async client => {
|
||||
let reviewTransfer: ReimportReviewTransferPlan | undefined;
|
||||
if (input.replacesScanRunId) {
|
||||
const replacedResult = await client.query<{ assessment_id: string; data: ScanRun } & QueryResultRow>(`
|
||||
SELECT assessment_id, data FROM assessor_scan_runs
|
||||
|
|
@ -736,6 +751,22 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
) !== 'match') {
|
||||
throw new RepositoryValidationError('Der Reimport muss kryptografisch exakt denselben Scanbericht ersetzen.');
|
||||
}
|
||||
const previousOccurrences = dataRows(await client.query<DataRow<FindingOccurrence>>(
|
||||
'SELECT data FROM assessor_finding_occurrences WHERE scan_run_id = $1 ORDER BY id',
|
||||
[input.replacesScanRunId]
|
||||
));
|
||||
const oldOccurrenceIds = previousOccurrences.map(item => item.id);
|
||||
const oldDefinitionIds = [...new Set(previousOccurrences.map(item => item.definitionId))];
|
||||
const reviews = dataRows(await client.query<DataRow<FindingReview>>(`
|
||||
SELECT data FROM assessor_reviews
|
||||
WHERE occurrence_id = ANY($1::text[]) OR definition_id = ANY($2::text[])
|
||||
ORDER BY id
|
||||
`, [oldOccurrenceIds, oldDefinitionIds]));
|
||||
reviewTransfer = planReimportReviewTransfer(previousOccurrences, model.findingOccurrences, reviews, run.importedAt);
|
||||
if (model.importFiles[0]) model.importFiles[0].reimportReviewTransfer = {
|
||||
replacesScanRunId: input.replacesScanRunId,
|
||||
...reviewTransfer.summary
|
||||
};
|
||||
const superseded = { ...replaced.data, supersededByScanRunId: run.id, supersededAt: run.importedAt };
|
||||
await client.query('UPDATE assessor_scan_runs SET data = $2::jsonb WHERE id = $1', [input.replacesScanRunId, json(superseded)]);
|
||||
}
|
||||
|
|
@ -817,6 +848,7 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
occurrenceImportFile
|
||||
);
|
||||
}
|
||||
if (reviewTransfer) await persistTransferredReviews(client, reviewTransfer.reviews);
|
||||
for (const record of model.evidence) await client.query(
|
||||
'INSERT INTO assessor_evidence (id, occurrence_id, data) VALUES ($1, $2, $3::jsonb)',
|
||||
[record.id, record.occurrenceId, json(record)]
|
||||
|
|
@ -852,7 +884,8 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
scanRunId: run.id,
|
||||
repositoryRevision: expectedRevision + 1,
|
||||
importedFiles: model.importFiles.length,
|
||||
importedOccurrences: model.findingOccurrences.length
|
||||
importedOccurrences: model.findingOccurrences.length,
|
||||
reimportReviewTransfer: reviewTransfer?.summary
|
||||
};
|
||||
}, expectedRevision);
|
||||
}
|
||||
|
|
@ -1155,38 +1188,49 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
}));
|
||||
}
|
||||
|
||||
private async unifiedPolicyObservationResults(
|
||||
query: UnifiedResultQuery = {},
|
||||
observationId?: string
|
||||
): Promise<UnifiedResultProjection[]> {
|
||||
if (query.audience !== 'internal-analysis') return [];
|
||||
if (query.sourceKind?.length && !query.sourceKind.includes('policy-observation')) return [];
|
||||
const where = query.projectId ? 'WHERE project_id = $1' : '';
|
||||
const values = query.projectId ? [query.projectId] : [];
|
||||
private async currentPolicyObservationResults(projectId?: string): Promise<UnifiedResultProjection[]> {
|
||||
const where = projectId ? 'WHERE project_id = $1' : '';
|
||||
const values = projectId ? [projectId] : [];
|
||||
const [artifactRows, processingRows, backupRows, analysisRows] = await Promise.all([
|
||||
this.pool.query<DataRow<ImportArtifact>>(`SELECT data FROM assessor_import_artifacts ${where} ORDER BY created_at, id`, values),
|
||||
this.pool.query<DataRow<ImportProcessingRun>>(`SELECT data FROM assessor_import_processing_runs ${where} ORDER BY completed_at, id`, values),
|
||||
this.pool.query<DataRow<StoredGpoBackupInstance>>(`SELECT data FROM assessor_gpo_backup_instances ${where} ORDER BY created_at, id`, values),
|
||||
this.pool.query<DataRow<StoredGpoPolicyAnalysis>>(`SELECT data FROM assessor_policy_identity_analysis_runs ${where} ORDER BY completed_at, id`, values)
|
||||
]);
|
||||
const results = createCurrentPolicyObservationUnifiedResults(
|
||||
return createCurrentPolicyObservationUnifiedResults(
|
||||
dataRows(artifactRows),
|
||||
dataRows(processingRows),
|
||||
dataRows(backupRows),
|
||||
dataRows(analysisRows)
|
||||
);
|
||||
}
|
||||
|
||||
private async unifiedPolicyObservationResults(
|
||||
query: UnifiedResultQuery = {},
|
||||
observationId?: string
|
||||
): Promise<UnifiedResultProjection[]> {
|
||||
if (query.audience !== 'internal-analysis') return [];
|
||||
if (query.sourceKind?.length && !query.sourceKind.includes('policy-observation')) return [];
|
||||
const results = await this.currentPolicyObservationResults(query.projectId);
|
||||
return observationId ? results.filter(item => item.sourceResultId === observationId) : results;
|
||||
}
|
||||
|
||||
private async unifiedPolicyConsistencyResults(query: UnifiedResultQuery = {}): Promise<UnifiedResultProjection[]> {
|
||||
if (query.audience === 'internal-analysis') return [];
|
||||
if (query.sourceKind?.length && !query.sourceKind.includes('policy-evaluation')) return [];
|
||||
return createPolicyConsistencyUnifiedResults(await this.currentPolicyObservationResults(query.projectId));
|
||||
}
|
||||
|
||||
private async allUnifiedResults(query: UnifiedResultQuery = {}): Promise<UnifiedResultProjection[]> {
|
||||
const [scanner, environment, policyObservations, policyEvaluations, policy] = await Promise.all([
|
||||
const [scanner, environment, policyObservations, policyConsistency, policyEvaluations, policy] = await Promise.all([
|
||||
this.unifiedScannerResults(query),
|
||||
this.unifiedEnvironmentResults(query),
|
||||
this.unifiedPolicyObservationResults(query),
|
||||
this.unifiedPolicyConsistencyResults(query),
|
||||
this.unifiedPolicyEvaluationComparisonResults(query),
|
||||
this.unifiedPolicyResults(query)
|
||||
]);
|
||||
return [...scanner, ...environment, ...policyObservations, ...policyEvaluations, ...policy];
|
||||
return [...scanner, ...environment, ...policyObservations, ...policyConsistency, ...policyEvaluations, ...policy];
|
||||
}
|
||||
|
||||
async queryUnifiedResults(query: UnifiedResultQuery = {}): Promise<UnifiedResultQueryResult> {
|
||||
|
|
@ -1202,6 +1246,7 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
const policyPrefix = 'policy-evaluation:';
|
||||
const policyComparisonPrefix = 'policy-evaluation-comparison:';
|
||||
const policyObservationPrefix = 'policy-observation:';
|
||||
const policyConsistencyPrefix = 'policy-consistency:';
|
||||
const environmentPrefix = 'environment-observation:';
|
||||
const items = resultProjectionId.startsWith(scannerPrefix)
|
||||
? await this.unifiedScannerResults({}, resultProjectionId.slice(scannerPrefix.length))
|
||||
|
|
@ -1211,6 +1256,8 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
? await this.unifiedPolicyResults({}, resultProjectionId.slice(policyPrefix.length))
|
||||
: resultProjectionId.startsWith(policyObservationPrefix)
|
||||
? await this.unifiedPolicyObservationResults({}, resultProjectionId.slice(policyObservationPrefix.length))
|
||||
: resultProjectionId.startsWith(policyConsistencyPrefix)
|
||||
? await this.unifiedPolicyConsistencyResults({})
|
||||
: resultProjectionId.startsWith(environmentPrefix)
|
||||
? await this.unifiedEnvironmentResults({}, resultProjectionId.slice(environmentPrefix.length))
|
||||
: [];
|
||||
|
|
@ -1222,11 +1269,11 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
return listUnifiedResultCategories(filterUnifiedResults(results, query));
|
||||
}
|
||||
|
||||
async adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string, supersededAt: string): Promise<void> {
|
||||
async adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string, supersededAt: string): Promise<ReimportReviewTransferSummary> {
|
||||
if (replacedScanRunId === replacementScanRunId || !Number.isFinite(Date.parse(supersededAt))) {
|
||||
throw new RepositoryValidationError('Die Scan-Ersatzverknüpfung ist ungültig.');
|
||||
}
|
||||
await this.mutate(async client => {
|
||||
return this.mutate(async client => {
|
||||
const selected = await client.query<{ assessment_id: string; project_id: string; data: ScanRun } & QueryResultRow>(`
|
||||
SELECT run.assessment_id, assessment.project_id, run.data
|
||||
FROM assessor_scan_runs run
|
||||
|
|
@ -1259,8 +1306,29 @@ export class PostgresAssessorRepository implements AssessorRepository {
|
|||
}
|
||||
const updatedReplaced = { ...replaced.data, supersededByScanRunId: replacementScanRunId, supersededAt };
|
||||
const updatedReplacement = { ...replacement.data, supersedesScanRunId: replacedScanRunId };
|
||||
const occurrenceRows = await client.query<{ scan_run_id: string; data: FindingOccurrence } & QueryResultRow>(`
|
||||
SELECT scan_run_id, data FROM assessor_finding_occurrences
|
||||
WHERE scan_run_id = ANY($1::text[]) ORDER BY scan_run_id, id
|
||||
`, [[replacedScanRunId, replacementScanRunId]]);
|
||||
const previousOccurrences = occurrenceRows.rows.filter(row => row.scan_run_id === replacedScanRunId).map(row => row.data);
|
||||
const replacementOccurrences = occurrenceRows.rows.filter(row => row.scan_run_id === replacementScanRunId).map(row => row.data);
|
||||
const occurrenceIds = occurrenceRows.rows.map(row => row.data.id);
|
||||
const definitionIds = [...new Set(occurrenceRows.rows.map(row => row.data.definitionId))];
|
||||
const reviews = dataRows(await client.query<DataRow<FindingReview>>(`
|
||||
SELECT data FROM assessor_reviews
|
||||
WHERE occurrence_id = ANY($1::text[]) OR definition_id = ANY($2::text[])
|
||||
ORDER BY id
|
||||
`, [occurrenceIds, definitionIds]));
|
||||
const reviewTransfer = planReimportReviewTransfer(previousOccurrences, replacementOccurrences, reviews, supersededAt);
|
||||
await persistTransferredReviews(client, reviewTransfer.reviews);
|
||||
const replacementFile = sourceFiles.rows.find(row => row.scan_run_id === replacementScanRunId)?.data;
|
||||
if (replacementFile) {
|
||||
const updatedFile = { ...replacementFile, reimportReviewTransfer: { replacesScanRunId: replacedScanRunId, ...reviewTransfer.summary } };
|
||||
await client.query('UPDATE assessor_import_files SET data = $2::jsonb WHERE id = $1', [updatedFile.id, json(updatedFile)]);
|
||||
}
|
||||
await client.query('UPDATE assessor_scan_runs SET data = $2::jsonb WHERE id = $1', [replacedScanRunId, json(updatedReplaced)]);
|
||||
await client.query('UPDATE assessor_scan_runs SET data = $2::jsonb WHERE id = $1', [replacementScanRunId, json(updatedReplacement)]);
|
||||
return reviewTransfer.summary;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ export class AssessorApiClient {
|
|||
return this.request('/reference-data/microsoft-security-baselines', { method: 'POST', body: JSON.stringify({ fileName: file.name, sizeBytes: file.size, bytesBase64: bytesToBase64(bytes) }) });
|
||||
}
|
||||
|
||||
adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string): Promise<{ repositoryRevision: number }> {
|
||||
adoptScanRunReplacement(projectId: string, replacedScanRunId: string, replacementScanRunId: string): Promise<{ repositoryRevision: number; reimportReviewTransfer: import('../domain/reimport-review-transfer').ReimportReviewTransferSummary }> {
|
||||
return this.request(`/projects/${encodeURIComponent(projectId)}/scan-runs/${encodeURIComponent(replacedScanRunId)}/adopt-replacement`, {
|
||||
method: 'POST', body: JSON.stringify({ replacementScanRunId })
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Assessment, ScanRun } from '../domain/canonical-model';
|
||||
import type { ReimportReviewTransferSummary } from '../domain/reimport-review-transfer';
|
||||
import type { ImportBatchPreview, ImportBatchSelection, ImportFingerprint } from '../domain/import-workflow';
|
||||
import type { MicrosoftSecurityBaselineRecord, MicrosoftSecurityBaselineSelection } from '../domain/microsoft-security-baseline';
|
||||
import type { StoredMicrosoftSecurityBaselineAnalysis } from '../domain/microsoft-security-baseline-analysis';
|
||||
|
|
@ -83,6 +84,7 @@ export interface ImportBatchCompleteResponse {
|
|||
replacedScanRunId?: string;
|
||||
importedFiles: number;
|
||||
importedOccurrences: number;
|
||||
reimportReviewTransfer?: ReimportReviewTransferSummary;
|
||||
}
|
||||
|
||||
export interface GpoBackupSubmissionRequest {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useApp } from './AppProvider';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { RetainedAppState, useApp } from './AppProvider';
|
||||
import { WORKSPACE_BAND_ID } from './workspace-band';
|
||||
import { DataHub } from '../features/project-import/DataHub';
|
||||
import { DashboardPanel } from '../features/dashboard/DashboardPanel';
|
||||
|
|
@ -67,10 +67,18 @@ export function App() {
|
|||
const [dataHubDetailClientName, setDataHubDetailClientName] = useState<string>();
|
||||
const [pathSnapshotRequest, setPathSnapshotRequest] = useState<{ id: string; path: PathSnapshotState }>();
|
||||
const [aboutOpen, setAboutOpen] = useState(false);
|
||||
const [visitedGraphScope, setVisitedGraphScope] = useState<string>();
|
||||
const graphVisited = visitedGraphScope === app.workScopeId;
|
||||
useEffect(() => { if (activeArea === 'graph') setVisitedGraphScope(app.workScopeId); }, [activeArea, app.workScopeId]);
|
||||
const clearPathSnapshotRequest = useCallback(() => setPathSnapshotRequest(undefined), []);
|
||||
const { dataset, activeSheet, reviews, snapshot, theme, setTheme, delimiter, setDelimiter, view, setView, actionOnly, sidebarCollapsed, toggleSidebar, toggleWorkspace, backendStatus, targetProjectId } = app;
|
||||
const openWork = useCallback(() => {
|
||||
setView('records');
|
||||
setActiveArea('work');
|
||||
}, [setView]);
|
||||
|
||||
|
||||
const marked = Object.values(reviews).filter(review => review.include).length;
|
||||
const marked = useMemo(() => Object.values(reviews).filter(review => review.include).length, [reviews]);
|
||||
const visible = useFilteredRecords().length;
|
||||
const backendLabel = backendStatus === 'connected' ? 'Server connection available' : backendStatus === 'checking' ? 'Checking server connection' : 'No server connection';
|
||||
const hasClientTimeslotScope = !targetProjectId
|
||||
|
|
@ -130,7 +138,7 @@ export function App() {
|
|||
</header>
|
||||
<main>
|
||||
<aside><div className="app-sidebar">
|
||||
<nav className="sidebar-primary-actions" aria-label="Main navigation">{NAV_AREAS.map(([area, label]) => <button className={`sidebar-tab-btn${activeArea === area ? ' active' : ''}`} key={area} type="button" disabled={area !== 'data-hub' && area !== 'db-structure' && !targetProjectId} onClick={() => setActiveArea(area)}><AreaIcon area={area}/><span>{label}</span></button>)}</nav>
|
||||
<nav className="sidebar-primary-actions" aria-label="Main navigation">{NAV_AREAS.map(([area, label]) => <button className={`sidebar-tab-btn${activeArea === area ? ' active' : ''}`} key={area} type="button" disabled={area !== 'data-hub' && area !== 'db-structure' && !targetProjectId} onClick={() => area === 'work' ? openWork() : setActiveArea(area)}><AreaIcon area={area}/><span>{label}</span></button>)}</nav>
|
||||
{activeArea === 'work' && <SidebarSeverityMetrics sheet={activeSheet}/>}
|
||||
{/* Die Sidebar traegt ausschliesslich Navigation. Der Serverzustand steht
|
||||
einheitlich als kompakte Rot-/Gruen-Anzeige im Bereichsheader. */}
|
||||
|
|
@ -145,13 +153,15 @@ export function App() {
|
|||
<section className={`workspace workspace-${activeArea}`}>
|
||||
<section className={`viewer app-area app-area-${activeArea}${activeArea === NAV_AREAS[0][0] ? ' is-joined-top' : ''}${activeArea === 'work' ? ` view-${view}` : ''}`}>
|
||||
{activeArea === 'work' && <DatasetViewer/>}
|
||||
{dataset && hasClientTimeslotScope && <div className="pathfinder-view-host" hidden={activeArea !== 'graph'}>
|
||||
<ObsidianGraphView
|
||||
active={activeArea === 'graph'}
|
||||
onOpenFinding={() => setActiveArea('work')}
|
||||
pathSnapshotRequest={pathSnapshotRequest}
|
||||
onPathSnapshotApplied={() => setPathSnapshotRequest(undefined)}
|
||||
/>
|
||||
{dataset && hasClientTimeslotScope && (graphVisited || activeArea === 'graph') && <div className="pathfinder-view-host" hidden={activeArea !== 'graph'}>
|
||||
<RetainedAppState key={app.workScopeId} active={activeArea === 'graph'}>
|
||||
<ObsidianGraphView
|
||||
active={activeArea === 'graph'}
|
||||
onOpenFinding={openWork}
|
||||
pathSnapshotRequest={pathSnapshotRequest}
|
||||
onPathSnapshotApplied={clearPathSnapshotRequest}
|
||||
/>
|
||||
</RetainedAppState>
|
||||
</div>}
|
||||
{activeArea === 'graph' && dataset && !hasClientTimeslotScope && <div className="datahub-empty"><h3>No timeslot selected</h3><p>Select a timeslot in Work to build its graph.</p></div>}
|
||||
{activeArea === 'data-hub' && (
|
||||
|
|
@ -172,7 +182,7 @@ export function App() {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{activeArea === 'dashboard' && <DashboardPanel onOpenWork={() => setActiveArea('work')}/>}
|
||||
{activeArea === 'dashboard' && <DashboardPanel onOpenWork={openWork}/>}
|
||||
{activeArea === 'db-structure' && <DbStructurePanel backendStatus={backendStatus}/>}
|
||||
{activeArea === 'results' && <ResultsBrowser backendStatus={backendStatus} projectId={targetProjectId} scanRunIds={app.activeScanRunIds} assessmentIds={app.activeAssessmentIds}/>}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -178,9 +178,12 @@ export function AppProvider({ children }: PropsWithChildren) {
|
|||
const checkBackend = async () => {
|
||||
try {
|
||||
const health = await assessorApi.health();
|
||||
if (active) setState(current => ({ ...current, backendStatus: health.status === 'ok' ? 'connected' : 'unavailable' }));
|
||||
if (active) setState(current => {
|
||||
const backendStatus = health.status === 'ok' ? 'connected' : 'unavailable';
|
||||
return current.backendStatus === backendStatus ? current : { ...current, backendStatus };
|
||||
});
|
||||
} catch {
|
||||
if (active) setState(current => ({ ...current, backendStatus: 'unavailable' }));
|
||||
if (active) setState(current => current.backendStatus === 'unavailable' ? current : { ...current, backendStatus: 'unavailable' });
|
||||
}
|
||||
};
|
||||
void checkBackend();
|
||||
|
|
@ -640,3 +643,11 @@ export function useApp(): AppContextValue {
|
|||
if (!context) throw new Error('useApp must be used within AppProvider.');
|
||||
return context;
|
||||
}
|
||||
|
||||
/** Retained views keep their last visible inputs without doing hidden derivation work. */
|
||||
export function RetainedAppState({ active, children }: PropsWithChildren<{ active: boolean }>) {
|
||||
const current = useApp();
|
||||
const lastVisible = useRef(current);
|
||||
if (active) lastVisible.current = current;
|
||||
return <AppContext.Provider value={lastVisible.current}>{children}</AppContext.Provider>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
let enabled = false;
|
||||
let steps: Record<string, number> = {};
|
||||
|
||||
/** UI-only instrumentation; production calls are just the original calculation. */
|
||||
export function measureNavigationStep<T>(name: string, compute: () => T): T {
|
||||
if (!import.meta.env.DEV || !enabled) return compute();
|
||||
const started = performance.now();
|
||||
try { return compute(); }
|
||||
finally { steps[name] = (steps[name] ?? 0) + performance.now() - started; }
|
||||
}
|
||||
|
||||
/** Opt-in, local development measurement. Never records report content. */
|
||||
export function observeNavigationPerformance(): void {
|
||||
enabled = true;
|
||||
let startedAt = 0;
|
||||
let target = '';
|
||||
let sequence = 0;
|
||||
let frames = 0;
|
||||
let previousFrame = 0;
|
||||
let longestFrame = 0;
|
||||
const output = document.createElement('output');
|
||||
output.id = 'assessor-navigation-performance';
|
||||
output.hidden = true;
|
||||
document.body.append(output);
|
||||
document.addEventListener('click', event => {
|
||||
const button = (event.target as Element).closest('.sidebar-primary-actions button');
|
||||
if (!button || button.hasAttribute('disabled')) return;
|
||||
target = button.textContent?.trim() ?? '';
|
||||
startedAt = performance.now();
|
||||
steps = {};
|
||||
sequence += 1;
|
||||
const current = sequence;
|
||||
previousFrame = startedAt;
|
||||
longestFrame = 0;
|
||||
frames = 0;
|
||||
const measure = (now: number) => {
|
||||
if (sequence !== current) return;
|
||||
longestFrame = Math.max(longestFrame, now - previousFrame);
|
||||
previousFrame = now;
|
||||
frames += 1;
|
||||
if (frames < 3) { requestAnimationFrame(measure); return; }
|
||||
output.textContent = JSON.stringify({ area: target, sequence: current,
|
||||
paintMs: Math.round(now - startedAt), longestFrameMs: Math.round(longestFrame),
|
||||
stepsMs: Object.fromEntries(Object.entries(steps).map(([name, duration]) => [name, Math.round(duration)])) });
|
||||
};
|
||||
requestAnimationFrame(measure);
|
||||
}, true);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import { highlightText } from '../HighlightText/highlight-text';
|
|||
import { BookmarkIcon } from '../BookmarkIcon';
|
||||
import { cisBenchmarkCategories } from '../../profiles/cis-cat/cis-benchmark-hierarchy';
|
||||
import { policyFindingCategories } from '../../profiles/group-policy/categories';
|
||||
import { measureNavigationStep } from '../../app/navigation-diagnostics';
|
||||
|
||||
export interface TileBadge {
|
||||
label: string;
|
||||
|
|
@ -82,7 +83,7 @@ function TileCardComponent({ record, title, bookmarked, resultColor, toolId, sea
|
|||
const severityField = record.fields.find(field => /^(severity|criticality|priority)$/i.test(field.label) && field.value);
|
||||
const severity = normalizeSeverity(severityField?.value);
|
||||
const recordToolId = toolIdForRecord(record, toolId);
|
||||
const badges = tileBadges(record, recordToolId);
|
||||
const badges = measureNavigationStep('tile-badges', () => tileBadges(record, recordToolId));
|
||||
const visibleBadges = compactTileBadges(badges, badgeLimit);
|
||||
const style = resultColor ? ({ '--result-color': resultColor } as CSSProperties) : undefined;
|
||||
const activate = (event: KeyboardEvent<HTMLElement>) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ResultContractExposureState, ResultContractResultType } from './result-contract-mapping';
|
||||
import type { EnvironmentObservation, ScannerReportSection } from './environment-context';
|
||||
import type { CveEnrichmentRecord } from './cve-enrichment';
|
||||
import type { SecurityScoringPolicy } from './security-scoring';
|
||||
|
||||
export type JsonPrimitive = string | number | boolean | null;
|
||||
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
|
||||
|
|
@ -94,6 +95,8 @@ export interface Assessment {
|
|||
scope?: string;
|
||||
/** Fachlicher Stichtag dieses vollständigen Mandanten-Bewertungsstands. */
|
||||
effectiveAt?: string;
|
||||
/** Persisted with the assessment JSON; absent historical values resolve to immutable v1. */
|
||||
scoringPolicy?: SecurityScoringPolicy;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
|
@ -132,6 +135,13 @@ export interface ImportFileRecord {
|
|||
detection: MappingConfidence;
|
||||
processedWith: ProcessingVersion;
|
||||
sourceArtifact: SourceArtifactContent;
|
||||
/** Persistent, content-free outcome of preserving client work during reimport. */
|
||||
reimportReviewTransfer?: {
|
||||
replacesScanRunId: string;
|
||||
transferred: number;
|
||||
retainedByStableIdentity: number;
|
||||
unresolved: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SourceTableRecord {
|
||||
|
|
@ -306,6 +316,9 @@ export interface FindingReview {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
revision: number;
|
||||
/** Audit reference when client-owned review data was carried to a reimported target. */
|
||||
transferredFromReviewId?: string;
|
||||
transferredAt?: string;
|
||||
}
|
||||
|
||||
export type DiagnosticSeverity = 'info' | 'warning' | 'error';
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { FindingRecord } from './finding';
|
|||
import type { ImportDocument } from './import-intermediate';
|
||||
import type { ImportRecognitionResult } from './import-recognition';
|
||||
import type { SourceExtractionResult } from './source-extraction';
|
||||
import type { SecurityScoreContext } from './security-scoring';
|
||||
|
||||
export type ColumnType = 'leer' | 'Datum' | 'Zahl' | 'Kategorie' | 'Text' | 'Text kurz';
|
||||
|
||||
|
|
@ -25,6 +26,31 @@ export interface SheetAnalysis {
|
|||
title: string;
|
||||
}
|
||||
|
||||
export interface ResultVariantSource {
|
||||
label: string;
|
||||
sourcePath: string;
|
||||
backupId: string;
|
||||
recordId: string;
|
||||
}
|
||||
|
||||
export interface ResultVariant {
|
||||
label: string;
|
||||
value: string;
|
||||
typeLabel: string;
|
||||
sources: ResultVariantSource[];
|
||||
}
|
||||
|
||||
export interface ResultVariantComparison {
|
||||
kind: 'variant-comparison';
|
||||
mode: 'conflict' | 'duplicate';
|
||||
title: string;
|
||||
scope: string;
|
||||
registryPath?: string;
|
||||
valueName?: string;
|
||||
variants: ResultVariant[];
|
||||
sourceCount: number;
|
||||
}
|
||||
|
||||
export interface DatasetSheet {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -37,6 +63,8 @@ export interface DatasetSheet {
|
|||
resultPreamble?: string;
|
||||
/** Columns whose differing evidence should be emphasized in Results. */
|
||||
resultEmphasisColumns?: string[];
|
||||
/** Structured, presentation-only comparison of conflicting evidence. */
|
||||
resultVariantComparison?: ResultVariantComparison;
|
||||
/** Stabiler Index der Quelltabelle innerhalb der Importdatei. */
|
||||
sourceTableIndex?: number;
|
||||
workbookIndex?: number;
|
||||
|
|
@ -51,6 +79,8 @@ export interface DatasetSheet {
|
|||
}
|
||||
|
||||
export interface ImportedDataset {
|
||||
/** Run-bound scoring evidence, prepared once while projecting the canonical workspace. */
|
||||
securityScoring?: SecurityScoreContext;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
lastModified: number;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ export interface FindingRecord {
|
|||
findingOccurrenceId?: string;
|
||||
/** Stable database identity shared by occurrences of the same finding. */
|
||||
findingDefinitionId?: string;
|
||||
/** Exact source scope for native report metrics; never inferred from display names. */
|
||||
scanRunId?: string;
|
||||
importFileId?: string;
|
||||
index: number;
|
||||
title: string;
|
||||
fields: RecordField[];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import type { GpoSecretIndicatorStatus } from './gpo-ingestion';
|
|||
|
||||
export interface GpoBackupMetadataDocument {
|
||||
version: 1;
|
||||
parser: { id: 'gpo-backup-metadata-xml'; version: '1.0.0' | '1.1.0' | '1.2.0' };
|
||||
parser: { id: 'gpo-backup-metadata-xml'; version: '1.0.0' | '1.1.0' | '1.2.0' | '1.3.0' };
|
||||
/** Only requested by the Microsoft template analyzer; never inferred from paths. */
|
||||
templateNames?: string[];
|
||||
sourcePath: string;
|
||||
sourceSha256: string;
|
||||
status: 'complete' | 'partial' | 'invalid';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { GpoPolicyObservation } from './gpo-policy';
|
|||
|
||||
export const MICROSOFT_BASELINE_SETTINGS_ANALYZER = {
|
||||
id: 'microsoft-baseline-gpo-settings',
|
||||
version: '1.1.0'
|
||||
version: '1.3.0'
|
||||
} as const;
|
||||
|
||||
export interface MicrosoftSecurityBaselineSetting {
|
||||
|
|
@ -11,6 +11,10 @@ export interface MicrosoftSecurityBaselineSetting {
|
|||
id: string;
|
||||
baselineId: string;
|
||||
backupId: string;
|
||||
template?: {
|
||||
names: Array<{ value: string; sourcePath: string; sourceSha256: string }>;
|
||||
context?: { productFamily: string; productVersion: string; role: 'Client' | 'Member Server' | 'Domain Controller' };
|
||||
};
|
||||
observation: Pick<GpoPolicyObservation,
|
||||
'sourcePath' | 'sourceSha256' | 'scope' | 'deliveryMechanism' | 'parser' | 'reducer' | 'state' | 'values'
|
||||
| 'registryPolicy' | 'sourceLines' | 'sourceRecords' | 'effectiveState' | 'diagnosticCodes'>;
|
||||
|
|
@ -22,7 +26,7 @@ export interface MicrosoftSecurityBaselineAnalysis {
|
|||
id: string;
|
||||
baselineId: string;
|
||||
packageSha256: string;
|
||||
analyzer: { id: typeof MICROSOFT_BASELINE_SETTINGS_ANALYZER.id; version: '1.0.0' | '1.1.0' };
|
||||
analyzer: { id: typeof MICROSOFT_BASELINE_SETTINGS_ANALYZER.id; version: '1.0.0' | '1.1.0' | '1.2.0' | '1.3.0' };
|
||||
analyzedAt: string;
|
||||
status: 'complete' | 'partial';
|
||||
admxCatalog: AdmxPolicyCatalog;
|
||||
|
|
@ -42,7 +46,7 @@ export function microsoftSecurityBaselineAnalysisValidationErrors(analysis: Micr
|
|||
const errors: string[] = [];
|
||||
if (analysis.version !== 1 || !analysis.id.trim() || !analysis.baselineId.trim() || !/^[a-f0-9]{64}$/.test(analysis.packageSha256)
|
||||
|| analysis.analyzer.id !== MICROSOFT_BASELINE_SETTINGS_ANALYZER.id
|
||||
|| !['1.0.0', '1.1.0'].includes(analysis.analyzer.version)
|
||||
|| !['1.0.0', '1.1.0', '1.2.0', '1.3.0'].includes(analysis.analyzer.version)
|
||||
|| !Number.isFinite(Date.parse(analysis.analyzedAt)) || analysis.admxCatalog.source.baselineId !== analysis.baselineId
|
||||
|| analysis.admxCatalog.source.packageSha256 !== analysis.packageSha256) {
|
||||
errors.push('Die Microsoft-Baselineanalyse besitzt ungültige Identität oder Provenienz.');
|
||||
|
|
@ -52,6 +56,14 @@ export function microsoftSecurityBaselineAnalysisValidationErrors(analysis: Micr
|
|||
errors.push('Die Microsoft-Baselineanalyse besitzt ungültige Zählwerte oder doppelte Einstellungen.');
|
||||
}
|
||||
for (const setting of analysis.settings) {
|
||||
const template = setting.template;
|
||||
if (template && (!Array.isArray(template.names) || template.names.some(name => typeof name.value !== 'string'
|
||||
|| !name.value.trim() || name.value.length > 2048 || typeof name.sourcePath !== 'string' || !name.sourcePath.trim()
|
||||
|| !/^[a-f0-9]{64}$/.test(name.sourceSha256))
|
||||
|| (template.context && (!template.names.length || !['Client', 'Member Server', 'Domain Controller'].includes(template.context.role)
|
||||
|| !template.context.productFamily?.trim() || !template.context.productVersion?.trim())))) {
|
||||
errors.push('Die Baselinevorlage besitzt ungueltige Namen, Rollen oder Quellenreferenzen.');
|
||||
}
|
||||
if (setting.version !== 1 || !setting.id.trim() || setting.baselineId !== analysis.baselineId || !setting.backupId.trim()
|
||||
|| !['registry-policy', 'security-template', 'advanced-audit-policy'].includes(setting.observation.deliveryMechanism)
|
||||
|| (setting.observation.deliveryMechanism === 'registry-policy' ? setting.observation.state !== 'instruction'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type PolicyEvaluationRuleCatalog
|
||||
} from './policy-evaluation-rules';
|
||||
|
||||
export const POLICY_EVALUATION_ENGINE = { id: 'policy-baseline-evaluation', version: '1.3.3' } as const;
|
||||
export const POLICY_EVALUATION_ENGINE = { id: 'policy-baseline-evaluation', version: '1.4.0' } as const;
|
||||
export const POLICY_COMPARATOR_CATALOG = { id: 'policy-comparator-catalog', version: '1.1.0' } as const;
|
||||
export const POLICY_DEPENDENCY_RULESET = { id: 'policy-dependency-rules', version: '1.1.0' } as const;
|
||||
export const POLICY_RISK_RULESET = { id: 'policy-risk-rules', version: '1.1.0' } as const;
|
||||
|
|
@ -80,6 +80,11 @@ export interface PolicyEvaluationResult {
|
|||
backupInstanceIds: string[];
|
||||
baselineAnalysisIds: string[];
|
||||
baselineSettingIds: string[];
|
||||
baselineTemplateSelection?: {
|
||||
status: 'selected' | 'unresolved' | 'no-match';
|
||||
candidateSettingIds: string[];
|
||||
contextIds: string[];
|
||||
};
|
||||
findingEligibility: 'eligible' | 'ineligible';
|
||||
effectiveState: 'unknown';
|
||||
diagnosticCodes: string[];
|
||||
|
|
@ -92,7 +97,8 @@ export interface PolicyEvaluationRun {
|
|||
aggregationRunId: string;
|
||||
environmentProfileId: string;
|
||||
baselineAnalysisIds: string[];
|
||||
evaluator: typeof POLICY_EVALUATION_ENGINE;
|
||||
/** Previous results remain readable; validation of new writes still requires the current engine. */
|
||||
evaluator: { id: typeof POLICY_EVALUATION_ENGINE.id; version: typeof POLICY_EVALUATION_ENGINE.version | '1.3.3' };
|
||||
comparatorCatalog: typeof POLICY_COMPARATOR_CATALOG;
|
||||
dependencyRules: typeof POLICY_DEPENDENCY_RULESET;
|
||||
riskRules: typeof POLICY_RISK_RULESET;
|
||||
|
|
@ -157,6 +163,13 @@ export function storedPolicyEvaluationValidationErrors(record: StoredPolicyEvalu
|
|||
|| Date.parse(run.completedAt) < Date.parse(run.startedAt)) errors.push('Die Laufzeit der Policy-Evaluation ist ungueltig.');
|
||||
const resultIds = new Set<string>();
|
||||
for (const result of results) {
|
||||
const selection = result.baselineTemplateSelection;
|
||||
if (selection && (!['selected', 'unresolved', 'no-match'].includes(selection.status)
|
||||
|| !uniqueSorted(selection.candidateSettingIds) || !uniqueSorted(selection.contextIds)
|
||||
|| result.baselineSettingIds.some(id => !selection.candidateSettingIds.includes(id))
|
||||
|| (selection.status === 'selected' && !selection.contextIds.length))) {
|
||||
errors.push('Die Auswahl der Baselinevorlagen besitzt ungueltige Referenzen.');
|
||||
}
|
||||
if (result.version !== 1 || !validText(result.id) || resultIds.has(result.id) || result.evaluationRunId !== run.id
|
||||
|| (result.aggregationStateId !== undefined && !validText(result.aggregationStateId))
|
||||
|| !validText(result.baselineAnalysisId) || !run.baselineAnalysisIds.includes(result.baselineAnalysisId)
|
||||
|
|
|
|||
|
|
@ -27,15 +27,20 @@ export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[]
|
|||
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false },
|
||||
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.1.0', mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'resultType', 'deviation'], current: true },
|
||||
{ profileId: 'purple-knight', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'purple-knight-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||
{ profileId: 'purple-knight', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: true },
|
||||
{ profileId: 'purple-knight', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||
{ profileId: 'purple-knight', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
|
||||
{ profileId: 'purple-knight', profileVersion: '1.3.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: true },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'pingcastle-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: false, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'presentation-only', reimportRequired: true, affectedDimensions: ['evidence', 'presentation'], current: false },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.3.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'presentation'], current: false },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.4.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.4.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
|
||||
{ profileId: 'pingcastle', profileVersion: '1.5.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: true },
|
||||
{ profileId: 'cis-cat', profileVersion: '1.2.0', mappingVersion: '1.2.0', normalizationVersion: '1.2.0', presentationVersion: '1.2.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence'], current: false },
|
||||
{ profileId: 'cis-cat', profileVersion: '1.3.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: ['evidence', 'presentation'], current: true },
|
||||
{ profileId: 'maester', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: true }
|
||||
{ profileId: 'cis-cat', profileVersion: '1.3.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: ['evidence', 'presentation'], current: false },
|
||||
{ profileId: 'cis-cat', profileVersion: '1.4.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true },
|
||||
{ profileId: 'maester', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false },
|
||||
{ profileId: 'maester', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.0.0', presentationVersion: '1.1.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence', 'environment-context', 'presentation'], current: true }
|
||||
] as const;
|
||||
|
||||
const manifestKey = (profileId: string, profileVersion: string) => `${profileId.trim().toLocaleLowerCase('en-US')}@${profileVersion.trim()}`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import type { FindingOccurrence, FindingReview } from './canonical-model';
|
||||
import { clean } from '../shared/text';
|
||||
|
||||
export interface ReimportReviewTransferSummary {
|
||||
transferred: number;
|
||||
retainedByStableIdentity: number;
|
||||
unresolved: number;
|
||||
}
|
||||
|
||||
export interface ReimportReviewTransferPlan {
|
||||
reviews: FindingReview[];
|
||||
summary: ReimportReviewTransferSummary;
|
||||
}
|
||||
|
||||
function normalized(value: unknown): string {
|
||||
return clean(value).toLocaleLowerCase('en-US');
|
||||
}
|
||||
|
||||
function occurrenceKey(occurrence: FindingOccurrence): string | undefined {
|
||||
const tool = normalized(occurrence.sourceIdentity.tool.id);
|
||||
const rule = normalized(occurrence.sourceIdentity.originalRuleId ?? occurrence.sourceIdentity.canonicalRuleId);
|
||||
if (!tool || !rule) return undefined;
|
||||
const asset = normalized(occurrence.affectedAsset?.normalizedValue ?? occurrence.affectedAsset?.originalValue);
|
||||
return JSON.stringify([tool, rule, asset]);
|
||||
}
|
||||
|
||||
function groupsByKey(occurrences: readonly FindingOccurrence[]): Map<string, FindingOccurrence[]> {
|
||||
const groups = new Map<string, FindingOccurrence[]>();
|
||||
for (const occurrence of occurrences) {
|
||||
const key = occurrenceKey(occurrence);
|
||||
if (!key) continue;
|
||||
groups.set(key, [...(groups.get(key) ?? []), occurrence]);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function transferId(reviewId: string, targetId: string): string {
|
||||
let value = 2166136261;
|
||||
for (const character of `${reviewId}\u0000${targetId}`) {
|
||||
value ^= character.charCodeAt(0);
|
||||
value = Math.imul(value, 16777619);
|
||||
}
|
||||
return `review-reimport-${(value >>> 0).toString(16).padStart(8, '0')}`;
|
||||
}
|
||||
|
||||
function transferredReview(review: FindingReview, target: FindingReview['target'], transferredAt: string): FindingReview {
|
||||
const targetId = target.kind === 'occurrence' ? target.occurrenceId : target.definitionId;
|
||||
return {
|
||||
...review,
|
||||
id: transferId(review.id, targetId),
|
||||
target,
|
||||
transferredFromReviewId: review.id,
|
||||
transferredAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Preserve scanner-independent client work when the exact source is reimported.
|
||||
* Matching is fail-closed: tool, native rule ID and asset must identify exactly
|
||||
* one old and one new occurrence. Unknown future review fields survive via the
|
||||
* object spread above.
|
||||
*/
|
||||
export function planReimportReviewTransfer(
|
||||
previous: readonly FindingOccurrence[],
|
||||
replacement: readonly FindingOccurrence[],
|
||||
reviews: readonly FindingReview[],
|
||||
transferredAt: string
|
||||
): ReimportReviewTransferPlan {
|
||||
const previousById = new Map(previous.map(item => [item.id, item]));
|
||||
const previousGroups = groupsByKey(previous);
|
||||
const replacementGroups = groupsByKey(replacement);
|
||||
const existingTargets = new Set(reviews.map(review => review.target.kind === 'occurrence'
|
||||
? `occurrence:${review.target.occurrenceId}` : `definition:${review.target.definitionId}`));
|
||||
const replacementsByDefinition = new Map<string, Set<string>>();
|
||||
for (const [key, oldItems] of previousGroups) {
|
||||
const newItems = replacementGroups.get(key) ?? [];
|
||||
if (oldItems.length !== 1 || newItems.length !== 1) continue;
|
||||
const targets = replacementsByDefinition.get(oldItems[0].definitionId) ?? new Set<string>();
|
||||
targets.add(newItems[0].definitionId);
|
||||
replacementsByDefinition.set(oldItems[0].definitionId, targets);
|
||||
}
|
||||
|
||||
const transferred: FindingReview[] = [];
|
||||
let retainedByStableIdentity = 0;
|
||||
let unresolved = 0;
|
||||
for (const review of reviews) {
|
||||
let target: FindingReview['target'] | undefined;
|
||||
if (review.target.kind === 'occurrence') {
|
||||
const oldOccurrence = previousById.get(review.target.occurrenceId);
|
||||
if (!oldOccurrence) continue;
|
||||
const key = occurrenceKey(oldOccurrence);
|
||||
const oldItems = key ? previousGroups.get(key) ?? [] : [];
|
||||
const newItems = key ? replacementGroups.get(key) ?? [] : [];
|
||||
if (oldItems.length === 1 && newItems.length === 1) target = { kind: 'occurrence', occurrenceId: newItems[0].id };
|
||||
} else {
|
||||
const definitionId = review.target.definitionId;
|
||||
if (!previous.some(item => item.definitionId === definitionId)) continue;
|
||||
if (replacement.some(item => item.definitionId === definitionId)) {
|
||||
retainedByStableIdentity += 1;
|
||||
continue;
|
||||
}
|
||||
const candidates = [...(replacementsByDefinition.get(definitionId) ?? [])];
|
||||
if (candidates.length === 1) target = { kind: 'definition', definitionId: candidates[0] };
|
||||
}
|
||||
if (!target) {
|
||||
unresolved += 1;
|
||||
continue;
|
||||
}
|
||||
const targetKey = target.kind === 'occurrence' ? `occurrence:${target.occurrenceId}` : `definition:${target.definitionId}`;
|
||||
if (existingTargets.has(targetKey)) {
|
||||
retainedByStableIdentity += 1;
|
||||
continue;
|
||||
}
|
||||
const copy = transferredReview(review, target, transferredAt);
|
||||
transferred.push(copy);
|
||||
}
|
||||
return { reviews: transferred, summary: { transferred: transferred.length, retainedByStableIdentity, unresolved } };
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/** Immutable scoring policy. Missing policies on historical assessments mean v1. */
|
||||
export interface SecurityScoringPolicy {
|
||||
version: '1.0.0';
|
||||
weights: Record<string, number>;
|
||||
defaultWeight: number;
|
||||
}
|
||||
|
||||
export function defaultSecurityScoringPolicy(): SecurityScoringPolicy {
|
||||
return { version: '1.0.0', weights: { 'purple-knight': 1, pingcastle: 1, 'cis-cat': 1, maester: 1, 'group-policy': 1 }, defaultWeight: 1 };
|
||||
}
|
||||
|
||||
export function validSecurityScoringPolicy(value: SecurityScoringPolicy): boolean {
|
||||
return Boolean(value && value.version === '1.0.0' && value.weights && typeof value.weights === 'object' && !Array.isArray(value.weights))
|
||||
&& Number.isFinite(value.defaultWeight) && value.defaultWeight > 0
|
||||
&& Object.values(value.weights).every(weight => Number.isFinite(weight) && weight > 0);
|
||||
}
|
||||
|
||||
export interface SecurityScanScore {
|
||||
id: string;
|
||||
assessmentId: string;
|
||||
toolId: string;
|
||||
toolName: string;
|
||||
runIds: string[];
|
||||
importFileIds: string[];
|
||||
occurrenceIds: string[];
|
||||
resultSignature?: string;
|
||||
evaluationScopeKey?: string;
|
||||
contentSha256?: string;
|
||||
/** Present only with an explicit assessed target and a known benchmark scope. */
|
||||
comparisonKey?: string;
|
||||
observedAt?: string;
|
||||
processingKey: string;
|
||||
policy: SecurityScoringPolicy;
|
||||
score?: number;
|
||||
nativeValue?: number;
|
||||
nativeProperty?: string;
|
||||
method: 'native' | 'inverted-native' | 'pass-rate' | 'unavailable';
|
||||
passed: number;
|
||||
deviations: number;
|
||||
assessed: number;
|
||||
eligible: number;
|
||||
openOccurrenceIds: string[];
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
export interface SecurityScoreContext {
|
||||
policy?: SecurityScoringPolicy;
|
||||
scans: SecurityScanScore[];
|
||||
baseline?: SecurityScanScore[];
|
||||
historyUnavailable?: boolean;
|
||||
}
|
||||
|
||||
export interface SecuritySourceScore {
|
||||
toolId: string;
|
||||
scans: SecurityScanScore[];
|
||||
score?: number;
|
||||
weight: number;
|
||||
methods: SecurityScanScore['method'][];
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
/** No intermediate rounding. Weight is per tool, never per imported file/check. */
|
||||
export function weightedSecurityScore(values: Array<{ score?: number; weight?: number }>): number | undefined {
|
||||
const scored = values.filter(value => value.score !== undefined && Number.isFinite(value.score)
|
||||
&& value.score >= 0 && value.score <= 100 && Number.isFinite(value.weight ?? 1) && (value.weight ?? 1) > 0);
|
||||
const denominator = scored.reduce((sum, value) => sum + (value.weight ?? 1), 0);
|
||||
return denominator ? scored.reduce((sum, value) => sum + value.score! * (value.weight ?? 1), 0) / denominator : undefined;
|
||||
}
|
||||
|
||||
export function securitySourceScores(scans: SecurityScanScore[]): SecuritySourceScore[] {
|
||||
const groups = new Map<string, SecurityScanScore[]>();
|
||||
for (const scan of scans) {
|
||||
const group = groups.get(scan.toolId);
|
||||
if (group) group.push(scan); else groups.set(scan.toolId, [scan]);
|
||||
}
|
||||
return [...groups].map(([toolId, items]) => {
|
||||
const policies = new Set(items.map(item => JSON.stringify([item.policy.version, item.policy.weights[toolId] ?? item.policy.defaultWeight])));
|
||||
const issues = [...new Set(items.flatMap(item => item.issues))];
|
||||
if (policies.size !== 1) issues.push('Scoring policies differ');
|
||||
return {
|
||||
toolId, scans: items,
|
||||
score: policies.size === 1 ? weightedSecurityScore(items) : undefined,
|
||||
weight: items[0].policy.weights[toolId] ?? items[0].policy.defaultWeight,
|
||||
methods: [...new Set(items.map(item => item.method))], issues
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Compare only the same target, benchmark, engine/processing and score method. */
|
||||
export function compareSecurityScores(baseline: SecurityScanScore[], current: SecurityScanScore[]) {
|
||||
const matching = (left: SecurityScanScore, right: SecurityScanScore) => left.comparisonKey !== undefined
|
||||
&& left.comparisonKey === right.comparisonKey && left.processingKey === right.processingKey
|
||||
&& left.evaluationScopeKey !== undefined && left.evaluationScopeKey === right.evaluationScopeKey
|
||||
&& left.observedAt !== undefined && right.observedAt !== undefined
|
||||
&& Date.parse(right.observedAt) > Date.parse(left.observedAt)
|
||||
&& left.issues.length === 0 && right.issues.length === 0
|
||||
&& left.method === right.method && left.policy.version === right.policy.version
|
||||
&& (left.policy.weights[left.toolId] ?? left.policy.defaultWeight) === (right.policy.weights[right.toolId] ?? right.policy.defaultWeight);
|
||||
const pairs = baseline.flatMap(left => {
|
||||
const candidates = current.filter(right => matching(left, right));
|
||||
return candidates.length === 1 && baseline.filter(other => matching(other, candidates[0])).length === 1
|
||||
&& left.score !== undefined && candidates[0].score !== undefined ? [{ left, right: candidates[0] }] : [];
|
||||
});
|
||||
const baselineScore = weightedSecurityScore(securitySourceScores(pairs.map(pair => pair.left)));
|
||||
const currentScore = weightedSecurityScore(securitySourceScores(pairs.map(pair => pair.right)));
|
||||
const unmatched = baseline.filter(left => !current.some(right => left.comparisonKey !== undefined
|
||||
&& left.comparisonKey === right.comparisonKey && right.observedAt !== undefined && left.observedAt !== undefined
|
||||
&& Date.parse(right.observedAt) > Date.parse(left.observedAt) && right.assessed > 0
|
||||
&& !right.issues.includes('Source integrity diagnostic')));
|
||||
return {
|
||||
baselineScore, currentScore,
|
||||
change: baselineScore === undefined || currentScore === undefined ? undefined : currentScore - baselineScore,
|
||||
comparable: pairs.length,
|
||||
scopeChanged: pairs.length !== baseline.length || pairs.length !== current.length,
|
||||
awaitingRevalidation: unmatched.map(scan => ({
|
||||
assessmentId: scan.assessmentId, runIds: scan.runIds, observedAt: scan.observedAt,
|
||||
occurrenceIds: scan.openOccurrenceIds
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
|
@ -69,6 +69,8 @@ export interface EnvironmentObservationCandidate {
|
|||
export interface SourceExtractorMatch {
|
||||
score: number;
|
||||
evidence: string[];
|
||||
/** Set by a registered extractor only after validating its native structure. */
|
||||
toolId?: string;
|
||||
}
|
||||
|
||||
export interface SourceExtractionResult {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ export interface ProfileSourceSignal extends WeightedPattern {
|
|||
export interface ProfileFieldRule {
|
||||
role: CanonicalFieldRole;
|
||||
headers: WeightedPattern[];
|
||||
/** Conditions apply only within the current source table/candidate shape. */
|
||||
whenHeadersPresent?: RegExp[];
|
||||
whenHeadersAbsent?: RegExp[];
|
||||
}
|
||||
|
||||
export interface ProfileValueMapping<T extends string> {
|
||||
|
|
@ -52,12 +55,35 @@ export interface ToolProcessingContract {
|
|||
affectedDimensions: string[];
|
||||
}
|
||||
|
||||
/** Every scanner must explicitly assess native overall-score support before fallback. */
|
||||
export interface ToolSecurityScoringDefinition {
|
||||
version: string;
|
||||
native: {
|
||||
kind: 'mapped';
|
||||
/** Exact mapped report properties, never fuzzy matches against finding fields. */
|
||||
properties: [string, ...string[]];
|
||||
scope: 'overall';
|
||||
scale: '0-100';
|
||||
direction: 'higher-is-better' | 'lower-is-better';
|
||||
/** Other native scopes that make this single contribution ambiguous. */
|
||||
incompatibleProperties?: string[];
|
||||
} | {
|
||||
kind: 'unavailable';
|
||||
/** Why the supported report formats cannot currently supply a usable native score. */
|
||||
reason: string;
|
||||
};
|
||||
targets: string[];
|
||||
benchmark?: string[];
|
||||
resultPopulation: 'complete' | 'findings-only' | 'unknown';
|
||||
}
|
||||
|
||||
export interface ToolProfileDefinition {
|
||||
schemaVersion: 1;
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
processing: ToolProcessingContract;
|
||||
securityScoring: ToolSecurityScoringDefinition;
|
||||
fallback?: boolean;
|
||||
sourceSignals: ProfileSourceSignal[];
|
||||
tableRules: {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ import {
|
|||
export type UnifiedResultSourceKind = 'scanner-finding' | 'policy-evaluation' | 'policy-observation' | 'environment-observation';
|
||||
export type UnifiedResultAudience = 'result-db' | 'internal-analysis';
|
||||
export type UnifiedOutcome = CanonicalOutcome;
|
||||
export type PolicyResultClassification =
|
||||
| 'misconfigured' | 'not-configured' | 'match' | 'conflict' | 'duplicate' | 'review-required';
|
||||
export type TechnicalExecutionState = CanonicalTechnicalExecutionState;
|
||||
export type UnifiedReviewStatus = 'unreviewed' | 'open' | 'in-progress' | 'resolved';
|
||||
export type UnifiedReviewDisposition = 'accepted' | 'deferred' | 'not-relevant';
|
||||
|
|
@ -113,6 +115,8 @@ export interface UnifiedResultProjection {
|
|||
policyActualValue?: JsonValue;
|
||||
policyComparisonRelation?: string;
|
||||
policyCustomerState?: PolicyEvaluationResult['customerState'];
|
||||
policyClassification?: PolicyResultClassification;
|
||||
policySourceCount?: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
recommendation?: string;
|
||||
|
|
@ -172,6 +176,7 @@ export interface UnifiedResultQuery {
|
|||
outcome?: UnifiedOutcome[];
|
||||
technicalExecutionState?: TechnicalExecutionState[];
|
||||
resultType?: CanonicalResultType[];
|
||||
policyClassification?: PolicyResultClassification[];
|
||||
isIoe?: boolean;
|
||||
deviation?: boolean;
|
||||
mappingRuleVersion?: string[];
|
||||
|
|
@ -208,7 +213,7 @@ export interface UnifiedResultFacet {
|
|||
id: 'sourceKind' | 'sourceTool' | 'severity' | 'outcome' | 'technicalExecutionState'
|
||||
| 'reviewStatus' | 'reviewDisposition' | 'lifecycleState' | 'effectiveState' | 'category'
|
||||
| 'resultType' | 'isIoe' | 'deviation' | 'mappingRuleVersion' | 'sourceProfileVersion' | 'processingStatus'
|
||||
| 'environmentState' | 'environmentProperty' | 'environmentSubjectType';
|
||||
| 'environmentState' | 'environmentProperty' | 'environmentSubjectType' | 'policyClassification';
|
||||
label: string;
|
||||
values: UnifiedResultFacetValue[];
|
||||
}
|
||||
|
|
@ -444,6 +449,7 @@ export function createPolicyUnifiedResult(source: PolicyUnifiedResultSource): Un
|
|||
isIoe: false,
|
||||
isDeviation: comparison ? policyComparisonDisposition(comparison) === 'misconfigured' : false,
|
||||
policyCustomerState: comparison?.customerState,
|
||||
policyClassification: comparison ? policyEvaluationClassification(comparison) : undefined,
|
||||
effectiveState: 'unknown_effective_state',
|
||||
lifecycleState: 'unknown',
|
||||
mappingConfidence: 1,
|
||||
|
|
@ -486,6 +492,15 @@ function policyEvaluationOutcome(result: PolicyEvaluationResult): UnifiedOutcome
|
|||
return 'warning';
|
||||
}
|
||||
|
||||
function policyEvaluationClassification(result: PolicyEvaluationResult): PolicyResultClassification {
|
||||
if (result.customerState === 'conflicted'
|
||||
|| result.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')) return 'conflict';
|
||||
if (policyComparisonDisposition(result) === 'not-configured') return 'not-configured';
|
||||
if (result.classification === 'configured_match') return 'match';
|
||||
if (policyComparisonDisposition(result) === 'misconfigured') return 'misconfigured';
|
||||
return 'review-required';
|
||||
}
|
||||
|
||||
function policyDeliveryMechanism(result: PolicyEvaluationResult): GpoPolicyObservation['deliveryMechanism'] {
|
||||
if (result.identity.target.kind === 'security-template') return 'security-template';
|
||||
if (result.identity.target.kind === 'advanced-audit-policy') return 'advanced-audit-policy';
|
||||
|
|
@ -564,6 +579,7 @@ export function createPolicyEvaluationUnifiedResult(
|
|||
policyActualValue: result.comparator.actual as JsonValue | undefined,
|
||||
policyComparisonRelation: result.comparator.relation,
|
||||
policyCustomerState: result.customerState,
|
||||
policyClassification: policyEvaluationClassification(result),
|
||||
categories: [{
|
||||
namespace: 'group-policy',
|
||||
categoryCode: 'GPOs',
|
||||
|
|
@ -577,16 +593,23 @@ export function createPolicyEvaluationUnifiedResult(
|
|||
};
|
||||
}
|
||||
|
||||
export function selectCurrentPolicyEvaluations(records: StoredPolicyEvaluation[]): StoredPolicyEvaluation[] {
|
||||
export function selectCurrentPolicyEvaluations(
|
||||
records: StoredPolicyEvaluation[],
|
||||
options: { allowPreviousVersionForDisplay?: boolean } = {}
|
||||
): StoredPolicyEvaluation[] {
|
||||
const selected = new Map<string, StoredPolicyEvaluation>();
|
||||
for (const record of records) {
|
||||
const isCurrent = record.run.evaluator.version === POLICY_EVALUATION_ENGINE.version;
|
||||
// Read-only Work compatibility, never a completion/finding-generation signal.
|
||||
const readablePrevious = options.allowPreviousVersionForDisplay && record.run.evaluator.version === '1.3.3';
|
||||
if (record.run.evaluator.id !== POLICY_EVALUATION_ENGINE.id
|
||||
|| record.run.evaluator.version !== POLICY_EVALUATION_ENGINE.version) continue;
|
||||
|| (!isCurrent && !readablePrevious)) continue;
|
||||
const current = selected.get(record.run.aggregationRunId);
|
||||
const replacesCurrent = !current
|
||||
|| record.run.baselineAnalysisIds.length > current.run.baselineAnalysisIds.length
|
||||
const sameVersion = current?.run.evaluator.version === record.run.evaluator.version;
|
||||
const replacesCurrent = !current || (isCurrent && !sameVersion)
|
||||
|| (sameVersion && (record.run.baselineAnalysisIds.length > current.run.baselineAnalysisIds.length
|
||||
|| (record.run.baselineAnalysisIds.length === current.run.baselineAnalysisIds.length
|
||||
&& `${record.run.completedAt}\u001f${record.run.id}` > `${current.run.completedAt}\u001f${current.run.id}`);
|
||||
&& `${record.run.completedAt}\u001f${record.run.id}` > `${current.run.completedAt}\u001f${current.run.id}`)));
|
||||
if (replacesCurrent) selected.set(record.run.aggregationRunId, record);
|
||||
}
|
||||
return [...selected.values()].sort((left, right) => left.run.completedAt.localeCompare(right.run.completedAt));
|
||||
|
|
@ -866,6 +889,7 @@ export function filterUnifiedResults(items: UnifiedResultProjection[], query: Un
|
|||
if (!includesAny(query.outcome, item.outcomeNormalized)) return false;
|
||||
if (!includesAny(query.technicalExecutionState, item.technicalExecutionState)) return false;
|
||||
if (!includesAny(query.resultType, item.resultType)) return false;
|
||||
if (!includesAny(query.policyClassification, item.policyClassification)) return false;
|
||||
if (query.isIoe !== undefined && item.isIoe !== query.isIoe) return false;
|
||||
if (query.deviation !== undefined && item.sourceKind !== 'policy-observation' && item.isDeviation !== query.deviation) return false;
|
||||
if (!includesAny(query.mappingRuleVersion, item.mappingRuleVersion)) return false;
|
||||
|
|
@ -907,6 +931,7 @@ export function filterUnifiedResults(items: UnifiedResultProjection[], query: Un
|
|||
policyActualValue: item.policyActualValue,
|
||||
policyComparisonRelation: item.policyComparisonRelation,
|
||||
policyCustomerState: item.policyCustomerState,
|
||||
policyClassification: item.policyClassification,
|
||||
categories: item.categories.map(category => category.categoryLabel)
|
||||
}));
|
||||
if (!text.includes(search)) return false;
|
||||
|
|
@ -1023,6 +1048,18 @@ export function deriveUnifiedResultFacets(items: UnifiedResultProjection[], quer
|
|||
facet('outcome', 'Outcome', item => findingAxis(item) ? item.outcomeNormalized : undefined, 'outcome'),
|
||||
facet('technicalExecutionState', 'Execution', item => findingAxis(item) ? item.technicalExecutionState : undefined, 'technicalExecutionState'),
|
||||
facet('resultType', 'Result Type', item => findingAxis(item) ? item.resultType : undefined, 'resultType'),
|
||||
(() => {
|
||||
const result = facet(
|
||||
'policyClassification', 'GPO Classification', item => item.policyClassification,
|
||||
'policyClassification', 'deviation'
|
||||
);
|
||||
const labels: Record<PolicyResultClassification, string> = {
|
||||
'misconfigured': 'Misconfigured', 'not-configured': 'Not configured', match: 'Match',
|
||||
conflict: 'Conflict', duplicate: 'Duplicate', 'review-required': 'Review required'
|
||||
};
|
||||
result.values.forEach(value => { value.label = labels[value.value as PolicyResultClassification] ?? value.label; });
|
||||
return result;
|
||||
})(),
|
||||
facet('isIoe', 'IOE', item => findingAxis(item) ? String(item.isIoe) : undefined, 'isIoe'),
|
||||
facet('deviation', 'Deviation', item => findingAxis(item) ? String(item.isDeviation) : undefined, 'deviation'),
|
||||
facet('mappingRuleVersion', 'Mapping Rule Set', item => item.mappingRuleVersion, 'mappingRuleVersion'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import type { MicrosoftSecurityBaselineSetting } from '../domain/microsoft-security-baseline-analysis';
|
||||
import type { EnvironmentProfile } from '../domain/gpo-snapshot-aggregation';
|
||||
|
||||
type TemplateContext = NonNullable<MicrosoftSecurityBaselineSetting['template']>['context'];
|
||||
|
||||
/** Deliberately narrow Microsoft template naming grammar, not a fuzzy role guess. */
|
||||
export function baselineTemplateContext(names: string[]): TemplateContext {
|
||||
const unique = [...new Set(names)];
|
||||
if (unique.length !== 1) return undefined;
|
||||
const name = unique[0].trim();
|
||||
const server = /^(?:MSFT )?Windows Server (2008 R2|2012 R2|2016|2019|2022|2025) - (Member Server|Domain Controller)$/i.exec(name);
|
||||
if (server) return { productFamily: 'Windows Server', productVersion: server[1].toUpperCase(),
|
||||
role: server[2].toLowerCase() === 'member server' ? 'Member Server' : 'Domain Controller' };
|
||||
const client = /^(?:MSFT )?Windows (10|11) (RS1|1607|1703|1709|1803|1809|1903|1909|2004|20H2|21H1|21H2|22H2|23H2|24H2|25H2) - (?:Computer|User)$/i.exec(name);
|
||||
if (client) return { productFamily: `Windows ${client[1]}`,
|
||||
productVersion: client[2].toUpperCase() === 'RS1' ? '1607' : client[2].toUpperCase(), role: 'Client' };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function selectBaselineTemplates(settings: MicrosoftSecurityBaselineSetting[], profile: EnvironmentProfile) {
|
||||
const scope = settings[0].observation.scope;
|
||||
const contexts = profile.contexts.filter(item => item.scope === 'mixed' || item.scope === scope);
|
||||
// Inventory-derived contexts and scope-only aggregation do not prove GPO applicability.
|
||||
if (contexts.length !== 1 || contexts[0].sourceObservationIds !== undefined) {
|
||||
return { settings, contextIds: [] as string[], status: 'unresolved' as const };
|
||||
}
|
||||
const context = contexts[0];
|
||||
const normalize = (value: string) => value.trim().toLowerCase();
|
||||
if (!['client', 'member server', 'domain controller'].includes(normalize(context.role))) {
|
||||
return { settings, contextIds: [] as string[], status: 'unresolved' as const };
|
||||
}
|
||||
// An unknown template could still apply: never drop it to manufacture a match.
|
||||
if (settings.some(item => !item.template?.context)) {
|
||||
return { settings, contextIds: [] as string[], status: 'unresolved' as const };
|
||||
}
|
||||
const selected = settings.filter(item => {
|
||||
const target = item.template!.context!;
|
||||
return normalize(target.productFamily) === normalize(context.productFamily)
|
||||
&& normalize(target.productVersion) === normalize(context.productVersion)
|
||||
&& normalize(target.role) === normalize(context.role);
|
||||
});
|
||||
return selected.length
|
||||
? { settings: selected, contextIds: [context.id], status: 'selected' as const }
|
||||
: { settings, contextIds: [context.id], status: 'no-match' as const };
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import type { GpoPolicyObservation } from '../domain/gpo-policy';
|
||||
import type { PolicyResultClassification, UnifiedResultProjection } from '../domain/unified-result';
|
||||
import { buildPolicyWorkResults } from './policy-work-results';
|
||||
|
||||
/**
|
||||
* Result-DB read projection for customer-side consistency findings. These are
|
||||
* deliberately separate from baseline deviations and do not mutate the
|
||||
* persisted observations or evaluations.
|
||||
*/
|
||||
export function createPolicyConsistencyUnifiedResults(
|
||||
observations: UnifiedResultProjection[]
|
||||
): UnifiedResultProjection[] {
|
||||
return buildPolicyWorkResults(observations, [])
|
||||
.filter(result => result.status === 'Conflicts' || result.status === 'Duplicates')
|
||||
.map(result => {
|
||||
const first = result.sources[0];
|
||||
const classification: PolicyResultClassification = result.status === 'Conflicts' ? 'conflict' : 'duplicate';
|
||||
return {
|
||||
resultProjectionId: `policy-consistency:${encodeURIComponent(result.id)}`,
|
||||
sourceKind: 'policy-evaluation',
|
||||
sourceResultId: result.id,
|
||||
projectId: first.projectId,
|
||||
assessmentId: first.assessmentId,
|
||||
collectionRunId: first.collectionRunId,
|
||||
gpoSnapshotId: first.gpoSnapshotId,
|
||||
sourceTool: 'GPOs',
|
||||
sourceProfileId: first.sourceProfileId,
|
||||
sourceProfileVersion: first.sourceProfileVersion,
|
||||
mappingRuleVersion: 'gpo-policy-consistency-1.0.0',
|
||||
normalizationRuleVersion: 'gpo-policy-consistency-1.0.0',
|
||||
presentationVersion: 'gpo-policy-consistency-1.0.0',
|
||||
processingCompatibilityKey: 'gpo-policy-consistency-v1',
|
||||
processingStatus: 'current',
|
||||
reprocessingAvailable: true,
|
||||
sourceRuleId: result.identity,
|
||||
title: result.title,
|
||||
description: classification === 'conflict'
|
||||
? 'Different customer values or types were observed for the same GPO setting.'
|
||||
: 'The same value and type were observed in multiple GPO source entries.',
|
||||
severityOriginal: null,
|
||||
severityNormalized: 'unknown',
|
||||
outcomeOriginal: result.status,
|
||||
outcomeNormalized: 'warning',
|
||||
technicalExecutionState: 'completed',
|
||||
resultType: 'informational',
|
||||
isIoe: false,
|
||||
isDeviation: false,
|
||||
effectiveState: 'unknown_effective_state',
|
||||
lifecycleState: 'unknown',
|
||||
mappingConfidence: 1,
|
||||
completenessState: 'complete',
|
||||
scopeState: 'unknown',
|
||||
effectivenessVerification: 'not-verified',
|
||||
scopeConfidence: 0,
|
||||
reviewStatus: 'open',
|
||||
importedAt: result.importedAt,
|
||||
createdAt: result.importedAt,
|
||||
policyScope: result.scope as GpoPolicyObservation['scope'],
|
||||
policyIdentityKey: result.identity,
|
||||
policyClassification: classification,
|
||||
policySourceCount: result.sources.length,
|
||||
policyDiagnosticCodes: [classification === 'conflict' ? 'policy-customer-conflict' : 'policy-customer-duplicate'],
|
||||
categories: [{
|
||||
namespace: 'group-policy-consistency',
|
||||
categoryCode: classification,
|
||||
categoryLabel: classification === 'conflict' ? 'Conflict' : 'Duplicate',
|
||||
categoryType: 'canonical',
|
||||
isPrimary: true,
|
||||
sourceTool: 'GPOs',
|
||||
sourceProfileId: first.sourceProfileId,
|
||||
sourceProfileVersion: first.sourceProfileVersion
|
||||
}]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ export interface PolicyWorkResult {
|
|||
scope: string;
|
||||
identity: string;
|
||||
importedAt: string;
|
||||
evaluatedAt?: string;
|
||||
sources: UnifiedResultProjection[];
|
||||
comparison?: PolicyEvaluationResult;
|
||||
}
|
||||
|
|
@ -68,6 +69,10 @@ function comparisonTitle(result: PolicyEvaluationResult): string {
|
|||
return target.valueName || ('section' in target ? `${target.section} · ${target.key}` : target.key);
|
||||
}
|
||||
|
||||
function latestImport(sources: UnifiedResultProjection[]): string {
|
||||
return sources.reduce((latest, source) => source.importedAt > latest ? source.importedAt : latest, '');
|
||||
}
|
||||
|
||||
/** Explicit comparison outcomes; absent settings remain separate from misconfiguration. */
|
||||
export function buildPolicyWorkResults(
|
||||
observations: UnifiedResultProjection[], evaluations: StoredPolicyEvaluation[]
|
||||
|
|
@ -94,33 +99,41 @@ export function buildPolicyWorkResults(
|
|||
if (status === 'Conflicts') group.forEach(source => conflictedIds.add(source.sourceResultId));
|
||||
output.push({ id: `${status}:${key}`, status, title: group[0].title,
|
||||
scope: group[0].policyScope!, identity: group[0].policyIdentityKey ?? key,
|
||||
importedAt: group.reduce((latest, source) => source.importedAt > latest ? source.importedAt : latest, ''), sources: group });
|
||||
importedAt: latestImport(group), sources: group });
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const evaluation of evaluations) for (const comparison of evaluation.results) {
|
||||
const id = comparison.id ?? JSON.stringify([comparison.baselineAnalysisId, comparison.baselineLabel,
|
||||
comparison.identity?.id, comparison.observationIds]);
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
// Never mix a comparison from another selected timeslot into this evidence set.
|
||||
if (comparison.observationIds.some(sourceId => !byId.has(sourceId))) continue;
|
||||
const evidence = comparison.observationIds.map(sourceId => byId.get(sourceId)!);
|
||||
const hasCustomerConflict = comparison.observationIds.some(sourceId => conflictedIds.has(sourceId));
|
||||
if (hasCustomerConflict) continue;
|
||||
const baselineConflict = comparison.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict');
|
||||
const baseline = comparison.comparator.baseline;
|
||||
const hasBaseline = baseline !== undefined && baseline.kind !== 'none'
|
||||
&& !(baseline.kind === 'text' && !baseline.value.trim());
|
||||
let status: PolicyWorkStatus;
|
||||
if (baselineConflict) status = 'Conflicts';
|
||||
else if (comparison.baselineState === 'explicit-recommendation' && hasBaseline
|
||||
&& comparison.customerState === 'configured' && comparison.comparator.actual !== undefined
|
||||
&& comparison.classification === 'configured_match' && comparison.comparator.relation === 'match') status = 'Pass';
|
||||
else if (policyComparisonDisposition(comparison) === 'misconfigured') status = 'Fail';
|
||||
else if (policyComparisonDisposition(comparison) === 'not-configured') status = 'Not configured';
|
||||
else continue;
|
||||
output.push({ id, status, title: comparisonTitle(comparison), scope: comparison.identity.scope,
|
||||
identity: comparison.identity.id, importedAt: evaluation.run.completedAt, sources: evidence, comparison });
|
||||
for (const evaluation of evaluations) {
|
||||
// Missing customer settings have no direct observations. Their import date
|
||||
// comes from the same evaluation's source context, never its execution time.
|
||||
const evaluationSources = [...new Set(evaluation.results.flatMap(result => result.observationIds))]
|
||||
.flatMap(id => byId.has(id) ? [byId.get(id)!] : []);
|
||||
const evaluationImportedAt = latestImport(evaluationSources);
|
||||
for (const comparison of evaluation.results) {
|
||||
const id = comparison.id ?? JSON.stringify([comparison.baselineAnalysisId, comparison.baselineLabel,
|
||||
comparison.identity?.id, comparison.observationIds]);
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
// Never mix a comparison from another selected timeslot into this evidence set.
|
||||
if (comparison.observationIds.some(sourceId => !byId.has(sourceId))) continue;
|
||||
const evidence = comparison.observationIds.map(sourceId => byId.get(sourceId)!);
|
||||
const hasCustomerConflict = comparison.observationIds.some(sourceId => conflictedIds.has(sourceId));
|
||||
if (hasCustomerConflict) continue;
|
||||
const baselineConflict = comparison.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict');
|
||||
const baseline = comparison.comparator.baseline;
|
||||
const hasBaseline = baseline !== undefined && baseline.kind !== 'none'
|
||||
&& !(baseline.kind === 'text' && !baseline.value.trim());
|
||||
let status: PolicyWorkStatus;
|
||||
if (baselineConflict) status = 'Conflicts';
|
||||
else if (comparison.baselineState === 'explicit-recommendation' && hasBaseline
|
||||
&& comparison.customerState === 'configured' && comparison.comparator.actual !== undefined
|
||||
&& comparison.classification === 'configured_match' && comparison.comparator.relation === 'match') status = 'Pass';
|
||||
else if (policyComparisonDisposition(comparison) === 'misconfigured') status = 'Fail';
|
||||
else if (policyComparisonDisposition(comparison) === 'not-configured') status = 'Not configured';
|
||||
else continue;
|
||||
output.push({ id, status, title: comparisonTitle(comparison), scope: comparison.identity.scope,
|
||||
identity: comparison.identity.id, importedAt: latestImport(evidence) || evaluationImportedAt,
|
||||
evaluatedAt: evaluation.run.completedAt, sources: evidence, comparison });
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useApp } from '../../app/AppProvider';
|
||||
import { objectsToCsv } from '../../exports/csv-export';
|
||||
import { buildDashboardHtml } from '../../exports/dashboard-export';
|
||||
|
|
@ -7,27 +8,28 @@ import { defaultReview } from '../../domain/review';
|
|||
|
||||
export function ActionlistControls() {
|
||||
const { dataset, reviews } = useApp();
|
||||
const rows = dataset?.sheets.flatMap(sheet => sheet.analysis.records
|
||||
.filter(record => reviews[record.id]?.include)
|
||||
.map(record => ({ record, sheetName: sheet.name, review: { ...defaultReview(), ...reviews[record.id] } }))) ?? [];
|
||||
|
||||
const exportRows = rows.map(({ record, sheetName, review }) => ({
|
||||
Sheet: sheetName,
|
||||
Record: record.index + 1,
|
||||
Finding: record.title,
|
||||
Status: review.status,
|
||||
Priority: review.priority,
|
||||
Owner: review.owner,
|
||||
DueDate: review.due,
|
||||
Prerequisites: review.prerequisites,
|
||||
AccessRights: review.access,
|
||||
NextStep: review.nextStep,
|
||||
Evidence: review.evidence,
|
||||
Comment: review.comment
|
||||
}));
|
||||
const hasRows = useMemo(() => Object.values(reviews).some(review => review.include)
|
||||
&& Boolean(dataset?.sheets.some(sheet => sheet.analysis.records.some(record => reviews[record.id]?.include))), [dataset, reviews]);
|
||||
|
||||
const exportActionList = (format: 'csv' | 'md' | 'html') => {
|
||||
if (!dataset || !rows.length) return;
|
||||
if (!dataset || !hasRows) return;
|
||||
const rows = dataset.sheets.flatMap(sheet => sheet.analysis.records
|
||||
.filter(record => reviews[record.id]?.include)
|
||||
.map(record => ({ record, sheetName: sheet.name, review: { ...defaultReview(), ...reviews[record.id] } })));
|
||||
const exportRows = rows.map(({ record, sheetName, review }) => ({
|
||||
Sheet: sheetName,
|
||||
Record: record.index + 1,
|
||||
Finding: record.title,
|
||||
Status: review.status,
|
||||
Priority: review.priority,
|
||||
Owner: review.owner,
|
||||
DueDate: review.due,
|
||||
Prerequisites: review.prerequisites,
|
||||
AccessRights: review.access,
|
||||
NextStep: review.nextStep,
|
||||
Evidence: review.evidence,
|
||||
Comment: review.comment
|
||||
}));
|
||||
const name = `${dataset.fileName.replace(/\.[^.]+$/, '')}-actionlist`;
|
||||
if (format === 'html') {
|
||||
downloadText(browserDownloadAdapter, `${name}.html`, buildDashboardHtml(dataset.fileName, rows), 'text/html;charset=utf-8');
|
||||
|
|
@ -41,10 +43,9 @@ export function ActionlistControls() {
|
|||
|
||||
return (
|
||||
<div className="header-export-row" aria-label="Export action list">
|
||||
<button className="btn export-btn" type="button" disabled={!rows.length} onClick={() => exportActionList('csv')}>CSV</button>
|
||||
<button className="btn export-btn" type="button" disabled={!rows.length} onClick={() => exportActionList('md')}>MD</button>
|
||||
<button className="btn export-btn" type="button" disabled={!rows.length} onClick={() => exportActionList('html')}>HTML</button>
|
||||
<button className="btn export-btn" type="button" disabled={!hasRows} onClick={() => exportActionList('csv')}>CSV</button>
|
||||
<button className="btn export-btn" type="button" disabled={!hasRows} onClick={() => exportActionList('md')}>MD</button>
|
||||
<button className="btn export-btn" type="button" disabled={!hasRows} onClick={() => exportActionList('html')}>HTML</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,45 @@
|
|||
import { Dialog } from '../../components/Dialog/Dialog';
|
||||
import '../../styles/security-scoring.css';
|
||||
import { compareSecurityScores, type SecurityScoreContext, type SecuritySourceScore } from '../../domain/security-scoring';
|
||||
|
||||
interface Props {
|
||||
sources: Array<{ tool: { id: string; label: string }; score?: number; weight?: number; passed: number; assessed: number; rating?: SecuritySourceScore }>;
|
||||
context?: SecurityScoreContext;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
const methodNames = { native: 'Native score', 'inverted-native': '100 − native risk', 'pass-rate': '100 × passed / assessed', unavailable: 'Evidence present, not scoreable' };
|
||||
const number = (value?: number) => value === undefined ? '—' : value.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
||||
|
||||
export function ScoringMethodology({ sources, context, onClose }: Props) {
|
||||
const comparison = context?.baseline ? compareSecurityScores(context.baseline, context.scans) : undefined;
|
||||
const pending = comparison?.awaitingRevalidation.reduce((count, item) => count + item.occurrenceIds.length, 0) ?? 0;
|
||||
return <Dialog title="Scoring methodology" onClose={onClose}>
|
||||
<div className="security-scoring-methodology">
|
||||
<p>The overall score is a combined assessment index. Each scan uses its validated native score, or the pass rate when the assessed results support that calculation. Native scores and pass rates use different methods; the index is not a percentage of security.</p>
|
||||
<p>Each source tool has a fixed weight. Its distinct, latest verified scan scopes contribute equally within that weight. Importing the same verified scan again adds no weight. Critical findings and assessment coverage remain separate.</p>
|
||||
<p>Security Domain numbers are pass rates, not components of the overall index. Not relevant results are excluded; unassessed and execution-error results do not become passed checks.</p>
|
||||
<table>
|
||||
<caption>Score contributions · Rule 1.0.0</caption>
|
||||
<thead><tr><th>Source</th><th>Contribution</th><th>Weight</th><th>Basis</th></tr></thead>
|
||||
<tbody>{sources.map(source => <tr key={source.tool.id}>
|
||||
<td>{source.tool.label}</td><td>{number(source.score)}</td><td>{number(source.weight ?? 1)}</td>
|
||||
<td>{source.rating ? source.rating.scans.map(scan => <div key={scan.id}>
|
||||
<strong>{methodNames[scan.method]}</strong>: {number(scan.score)}
|
||||
{scan.nativeValue !== undefined && <> · Native: {number(scan.nativeValue)}</>}
|
||||
<div>{scan.assessed} assessed · {scan.eligible} known eligible · Scan time: {scan.observedAt ?? '—'}</div>
|
||||
{scan.issues.length > 0 && <div>{scan.issues.join(' · ')}</div>}
|
||||
</div>) : <>{source.passed} / {source.assessed} passed · Pass rate</>}</td>
|
||||
</tr>)}</tbody>
|
||||
</table>
|
||||
<p>Values are rounded only for display. A missing, invalid or ambiguous native score is disclosed. Historical assessments without an explicit policy use the fixed version 1.0.0 policy.</p>
|
||||
{context?.historyUnavailable && <p>Historical evidence could not be fully loaded. No verified progress is available.</p>}
|
||||
{comparison && !context?.historyUnavailable && <>
|
||||
<h3>Comparable progress</h3>
|
||||
<p>{comparison.comparable} comparable scan scopes · Change: {comparison.change === undefined ? '—' : `${comparison.change > 0 ? '+' : ''}${number(comparison.change)} points`}{comparison.scopeChanged ? ' · Scope or method changed; overall values are not directly comparable.' : ''}</p>
|
||||
<p>{pending} previously open findings await revalidation. Historical risks retain their original scan reference and are not reported as resolved because a follow-up scan is missing.</p>
|
||||
{comparison.awaitingRevalidation.filter(item => item.occurrenceIds.length > 0).map((item, index) => <p key={`${item.assessmentId}-${index}`}>{item.occurrenceIds.length} open findings · Last observed: {item.observedAt ?? '—'}</p>)}
|
||||
</>}
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import type { FindingRecord } from '../../domain/finding';
|
||||
import { securitySourceScores, validSecurityScoringPolicy, type SecurityScoreContext, type SecuritySourceScore } from '../../domain/security-scoring';
|
||||
import { securityToolId } from '../../scoring/canonical-security-score';
|
||||
|
||||
interface ScoredSource {
|
||||
tool: { id: string; label: string; sourceValues: string[]; count: number };
|
||||
records: FindingRecord[];
|
||||
passed: number;
|
||||
deviations: number;
|
||||
assessed: number;
|
||||
score?: number;
|
||||
weight?: number;
|
||||
rating?: SecuritySourceScore;
|
||||
}
|
||||
|
||||
export function dashboardSecuritySources(sources: ScoredSource[], context?: SecurityScoreContext, includeContextOnly = false): ScoredSource[] {
|
||||
if (!context) return sources;
|
||||
const result = sources.map(source => {
|
||||
const files = new Set(source.records.flatMap(record => record.importFileId ? [record.importFileId] : []));
|
||||
const toolId = securityToolId(source.tool.label);
|
||||
const matching = context.scans.filter(scan => scan.toolId === toolId && scan.importFileIds.some(id => files.has(id)));
|
||||
const rating = securitySourceScores(matching)[0];
|
||||
// A bound source without a usable native score remains explicitly unscoreable.
|
||||
// GPO and local tabular sources keep the existing context-aware pass-rate path.
|
||||
if (rating) return { ...source, score: rating.score, weight: rating.weight, rating };
|
||||
const policy = context.policy;
|
||||
return policy ? { ...source, score: validSecurityScoringPolicy(policy) ? source.score : undefined,
|
||||
weight: policy.weights?.[toolId] ?? policy.defaultWeight } : source;
|
||||
});
|
||||
if (includeContextOnly) {
|
||||
const present = new Set(sources.map(source => securityToolId(source.tool.label)));
|
||||
for (const rating of securitySourceScores(context.scans).filter(source => !present.has(source.toolId))) {
|
||||
result.push({
|
||||
tool: { id: rating.toolId, label: rating.scans[0].toolName, sourceValues: [], count: 0 }, records: [],
|
||||
passed: rating.scans.reduce((sum, scan) => sum + scan.passed, 0),
|
||||
deviations: rating.scans.reduce((sum, scan) => sum + scan.deviations, 0),
|
||||
assessed: rating.scans.reduce((sum, scan) => sum + scan.assessed, 0),
|
||||
score: rating.score, weight: rating.weight, rating
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useApp } from '../../app/AppProvider';
|
||||
import { TriangleIcon } from '../../components/TriangleIcon';
|
||||
|
|
@ -82,7 +82,8 @@ interface OpenResultWindow {
|
|||
detached: boolean;
|
||||
}
|
||||
|
||||
import { highlightText } from '../../components/HighlightText/highlight-text';
|
||||
import { RawDataView } from './RawDataView';
|
||||
import { measureNavigationStep } from '../../app/navigation-diagnostics';
|
||||
|
||||
export function DatasetViewer() {
|
||||
const app = useApp();
|
||||
|
|
@ -127,13 +128,6 @@ export function DatasetViewer() {
|
|||
left: Math.max(0, Math.min(requestedClusterPos.left, maxClusterLeft)),
|
||||
top: Math.max(minClusterTop, Math.min(requestedClusterPos.top, maxClusterTop))
|
||||
};
|
||||
const recordTitles = useMemo(() => {
|
||||
if (!activeSheet) return new Map<string, string>();
|
||||
return new Map<string, string>(activeSheet.analysis.records.map(record => [
|
||||
record.id,
|
||||
recordTitleFor(record, titleColumn, activeSheet.toolId ?? dataset?.profileId)
|
||||
]));
|
||||
}, [activeSheet, dataset?.profileId, titleColumn]);
|
||||
const resultColorsByRecord = useMemo(() => new Map(resultWindows.map(item => [item.recordId, item.color])), [resultWindows]);
|
||||
|
||||
// Kacheln: die Spaltenzahl treibt sowohl das CSS-Grid als auch die
|
||||
|
|
@ -160,14 +154,14 @@ export function DatasetViewer() {
|
|||
// Zwei Refs auf demselben Knoten: `tileScrollRef` misst die Breite fuer die
|
||||
// Spaltenzahl, `tileWindow.containerRef` haengt den Scroll-Listener der
|
||||
// Fenster-Berechnung an.
|
||||
const setTileScrollNode = (node: HTMLDivElement | null) => {
|
||||
const setTileScrollNode = useCallback((node: HTMLDivElement | null) => {
|
||||
tileScrollRef.current = node;
|
||||
tileWindow.containerRef(node);
|
||||
if (node && node.clientWidth > 0) {
|
||||
const contentWidth = node.clientWidth - TILE_GRID_INSET * 2;
|
||||
setTileColumns(Math.max(1, Math.floor((contentWidth + TILE_GAP) / (TILE_MIN_WIDTH + TILE_GAP))));
|
||||
}
|
||||
};
|
||||
}, [tileWindow.containerRef]);
|
||||
|
||||
// Datensaetze: die aktive Karte kann ausserhalb des gerenderten Fensters
|
||||
// liegen (Weiter/Zurueck im Leser, oder Einstieg aus den Kacheln auf einen
|
||||
|
|
@ -230,7 +224,8 @@ export function DatasetViewer() {
|
|||
|
||||
if (!dataset || !activeSheet) return <EmptyWorkspace />;
|
||||
|
||||
const recordTitle = (record: FindingRecord) => recordTitles.get(record.id) ?? record.title;
|
||||
const recordTitle = (record: FindingRecord) =>
|
||||
recordTitleFor(record, titleColumn, activeSheet.toolId ?? dataset.profileId);
|
||||
const selected = records.find(record => record.id === selectedRecordId) ?? records[0];
|
||||
const toolId = activeSheet.toolId ?? dataset.profileId;
|
||||
const linksFor = (record: FindingRecord) => [
|
||||
|
|
@ -240,7 +235,7 @@ export function DatasetViewer() {
|
|||
];
|
||||
const messageFor = (record: FindingRecord) => resultMessageFor(record, activeSheet.toolId ?? dataset.profileId);
|
||||
const sectionsFor = (record: FindingRecord) => resultSectionsFor(record, activeSheet.toolId ?? dataset.profileId);
|
||||
const hasResults = (record: FindingRecord) => linksFor(record).length > 0 || sectionsFor(record).length > 0;
|
||||
const hasResults = (record: FindingRecord) => measureNavigationStep('result-links', () => linksFor(record).length > 0 || sectionsFor(record).length > 0);
|
||||
const activateWindow = (recordId: string) => setResultWindows(current => current.map(item => item.recordId === recordId ? { ...item, zIndex: ++nextZIndex.current } : item));
|
||||
const openResults = (record: FindingRecord) => {
|
||||
const sheets = linksFor(record);
|
||||
|
|
@ -311,10 +306,6 @@ export function DatasetViewer() {
|
|||
setView('records');
|
||||
};
|
||||
|
||||
const rawTextContent = search || app.actionOnly || app.statusFilter !== 'all' || Object.keys(app.facetFilters).length
|
||||
? records.map(record => [`# Record ${record.index + 1}`, ...record.fields.filter(field => field.value).map(field => `${field.label}: ${field.value}`)].join('\n')).join('\n\n')
|
||||
: activeSheet.raw || dataset.rawText;
|
||||
|
||||
// Berechnete Geometrie der Virtualisierung: als benannte Werte statt eines
|
||||
// literalen Inline-Style-Objekts, damit die Darstellung sonst in
|
||||
// src/styles bleibt (siehe FloatingResultsWindow.tsx fuer denselben
|
||||
|
|
@ -373,7 +364,8 @@ export function DatasetViewer() {
|
|||
) : view === 'table' ? (
|
||||
<DataTable sheet={activeSheet} records={records} search={search} />
|
||||
) : (
|
||||
<div className="viewer-scroll"><pre className="raw-view">{highlightText(rawTextContent, search)}</pre></div>
|
||||
<RawDataView records={records} raw={activeSheet.raw || dataset.rawText}
|
||||
filtered={Boolean(search || app.actionOnly || app.statusFilter !== 'all' || Object.keys(app.facetFilters).length)} search={search}/>
|
||||
)}
|
||||
|
||||
{clusterDocked && dockSlot && createPortal(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import type { FindingRecord } from '../../domain/finding';
|
||||
import { highlightText } from '../../components/HighlightText/highlight-text';
|
||||
|
||||
/** Building the complete text belongs exclusively to the Raw Data view. */
|
||||
export const RawDataView = memo(function RawDataView({ records, raw, filtered, search }: {
|
||||
records: FindingRecord[]; raw: string; filtered: boolean; search: string;
|
||||
}) {
|
||||
const text = useMemo(() => filtered
|
||||
? records.map(record => [`# Record ${record.index + 1}`,
|
||||
...record.fields.filter(field => field.value).map(field => `${field.label}: ${field.value}`)].join('\n')).join('\n\n')
|
||||
: raw, [filtered, raw, records]);
|
||||
return <div className="viewer-scroll"><pre className="raw-view">{highlightText(text, search)}</pre></div>;
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ import { ResizableResultTable } from '../../components/ResizableResultTable/Resi
|
|||
import type { ResultSection } from './result-message';
|
||||
import type { ResultSheetRowTone } from './result-message';
|
||||
import type { MaesterExecutionError } from './maester-error';
|
||||
import { VariantComparisonResult } from './VariantComparisonResult';
|
||||
|
||||
function MaesterErrorResult({ error, search }: { error: MaesterExecutionError; search: string }) {
|
||||
const facts = [
|
||||
|
|
@ -70,9 +71,10 @@ function highlighted(value: string, search: string): ReactNode {
|
|||
*/
|
||||
export function LinkedResults({ sheets, search = '', heading, sections = [], showSheetTitles = true, sheetTitle, sheetSubtitle, rowHighlighted, rowTone, resizableTables = false }: { sheets: DatasetSheet[]; search?: string; heading?: string; sections?: ResultSection[]; showSheetTitles?: boolean; sheetTitle?: (sheet: DatasetSheet) => string | undefined; sheetSubtitle?: (sheet: DatasetSheet) => string | undefined; rowHighlighted?: (record: DatasetSheet['analysis']['records'][number]) => boolean; rowTone?: (record: DatasetSheet['analysis']['records'][number]) => ResultSheetRowTone | undefined; resizableTables?: boolean }) {
|
||||
if (!sheets.length && !sections.length) return <div className="linked-results-empty">No matching result file found automatically.</div>;
|
||||
const variantComparison = sheets.find(sheet => sheet.resultVariantComparison)?.resultVariantComparison;
|
||||
return (
|
||||
<div className="linked-results">
|
||||
{heading && <h3 className="linked-results-heading">{heading}</h3>}
|
||||
{heading && !variantComparison && <h3 className="linked-results-heading">{heading}</h3>}
|
||||
{/* Die Ergebnistexte stehen vor den verknuepften Blaettern: sie sind die
|
||||
Aussage des Werkzeugs, die Blaetter sind ihre Belegdaten. */}
|
||||
{sections.map(section => (
|
||||
|
|
@ -83,7 +85,9 @@ export function LinkedResults({ sheets, search = '', heading, sections = [], sho
|
|||
: <RichTextValue value={section.value} search={search} resizableTables={resizableTables} />}
|
||||
</section>
|
||||
))}
|
||||
{variantComparison && <VariantComparisonResult comparison={variantComparison} search={search} />}
|
||||
{sheets.map((sheet, index) => {
|
||||
if (variantComparison && (sheet.resultVariantComparison || sheet.name === 'Source references')) return null;
|
||||
const displayTitle = showSheetTitles ? (sheetTitle?.(sheet) ?? sheet.name) : undefined;
|
||||
const previousTitle = index > 0 && sheetTitle ? sheetTitle(sheets[index - 1]) : undefined;
|
||||
const displaySubtitle = sheetSubtitle?.(sheet);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import type { ResultVariantComparison, ResultVariantSource } from '../../domain/dataset';
|
||||
import { PathText } from '../../components/PathText/PathText';
|
||||
|
||||
function highlighted(value: string, search: string): ReactNode {
|
||||
if (!search) return value;
|
||||
const lower = value.toLocaleLowerCase('en-US');
|
||||
const parts: ReactNode[] = [];
|
||||
let cursor = 0;
|
||||
let found = lower.indexOf(search.toLocaleLowerCase('en-US'), cursor);
|
||||
while (found >= 0) {
|
||||
if (found > cursor) parts.push(value.slice(cursor, found));
|
||||
parts.push(<mark key={`${found}-${cursor}`}>{value.slice(found, found + search.length)}</mark>);
|
||||
cursor = found + search.length;
|
||||
found = lower.indexOf(search.toLocaleLowerCase('en-US'), cursor);
|
||||
}
|
||||
if (cursor < value.length) parts.push(value.slice(cursor));
|
||||
return parts;
|
||||
}
|
||||
|
||||
function DisplayValue({ value, search }: { value: string; search: string }) {
|
||||
if (value === '') return <><code className="variant-comparison__literal">""</code><span className="variant-comparison__value-note">Empty string</span></>;
|
||||
if (/^\s+$/.test(value)) return <><code className="variant-comparison__literal">{highlighted(JSON.stringify(value), search)}</code>
|
||||
<span className="variant-comparison__value-note">{value.length} whitespace {value.length === 1 ? 'character' : 'characters'}</span></>;
|
||||
return <PathText value={value} search={search} label="Value" />;
|
||||
}
|
||||
|
||||
function SourceList({ sources, search }: { sources: ResultVariantSource[]; search: string }) {
|
||||
return <div className="variant-comparison__sources" aria-label={sources.length === 1 ? 'GPO source' : 'GPO sources'}>
|
||||
{sources.map((source, index) => <PathText key={`${source.recordId}-${index}`} value={source.sourcePath} search={search} label="Source path" />)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
function TechnicalSources({ comparison, search }: { comparison: ResultVariantComparison; search: string }) {
|
||||
return <details className="variant-comparison__technical">
|
||||
<summary>Technical source details</summary>
|
||||
<div className="variant-comparison__technical-list">{comparison.variants.flatMap(variant => variant.sources.map((source, index) =>
|
||||
<section key={`${variant.label}-${source.recordId}-${index}`}>
|
||||
<h5>{source.label} · {variant.label}</h5>
|
||||
<dl>
|
||||
<div><dt>Source path</dt><dd><PathText value={source.sourcePath} search={search} label="Source path" /></dd></div>
|
||||
<div><dt>Backup ID</dt><dd>{highlighted(source.backupId, search)}</dd></div>
|
||||
<div><dt>Record ID</dt><dd>{highlighted(source.recordId, search)}</dd></div>
|
||||
</dl>
|
||||
</section>))}</div>
|
||||
</details>;
|
||||
}
|
||||
|
||||
export function VariantComparisonResult({ comparison, search }: { comparison: ResultVariantComparison; search: string }) {
|
||||
const sourceNoun = comparison.sourceCount === 1 ? 'GPO source defines' : 'GPO sources define';
|
||||
const duplicate = comparison.mode === 'duplicate';
|
||||
return <section className={`variant-comparison${duplicate ? ' is-duplicate' : ''}`} aria-label={duplicate ? 'Duplicate setting evidence' : 'Conflicting setting comparison'}>
|
||||
<header className="variant-comparison__header">
|
||||
<p className="variant-comparison__eyebrow">{duplicate ? 'Duplicate setting' : 'Conflicting setting'}</p>
|
||||
<h3>{highlighted(comparison.title, search)}</h3>
|
||||
<p className="variant-comparison__scope">{comparison.scope ? `${comparison.scope[0].toUpperCase()}${comparison.scope.slice(1)} scope` : 'Policy scope'}</p>
|
||||
</header>
|
||||
<div className={`variant-comparison__notice${duplicate ? ' is-duplicate' : ''}`}>
|
||||
<strong>{duplicate ? 'Duplicate detected' : 'Conflict detected'}</strong>
|
||||
<p>{duplicate
|
||||
? `${comparison.sourceCount} ${sourceNoun} this setting with an identical value and type.`
|
||||
: `${comparison.sourceCount} ${sourceNoun} this setting with ${comparison.variants.length} different values or types.`}</p>
|
||||
<p>{duplicate
|
||||
? 'This may be intentional. A duplicate alone is not a baseline deviation.'
|
||||
: 'ASSESSOR shows the conflicting evidence but does not determine which value ultimately applies.'}</p>
|
||||
</div>
|
||||
<section className="variant-comparison__setting">
|
||||
<h4>Setting details</h4>
|
||||
<dl>
|
||||
{comparison.registryPath && <div><dt>Registry path</dt><dd><PathText value={comparison.registryPath} search={search} label="Registry path" /></dd></div>}
|
||||
{comparison.valueName && <div><dt>Value name</dt><dd>{highlighted(comparison.valueName, search)}</dd></div>}
|
||||
{comparison.scope && <div><dt>Scope</dt><dd>{highlighted(comparison.scope, search)}</dd></div>}
|
||||
</dl>
|
||||
</section>
|
||||
<section className="variant-comparison__variants">
|
||||
<h4>{duplicate ? 'Observed value' : 'Observed variants'}</h4>
|
||||
<p>{duplicate ? 'The following value and type were found in every listed source.' : 'The following values or types were found for the affected setting.'}</p>
|
||||
<div className="variant-comparison__table-wrap">
|
||||
<table className="variant-comparison__table">
|
||||
<thead><tr>{!duplicate && <th>Variant</th>}<th>Value</th><th>Type</th><th>Evidence</th></tr></thead>
|
||||
<tbody>{comparison.variants.map(variant => <tr key={variant.label}>
|
||||
{!duplicate && <th scope="row">{variant.label}</th>}
|
||||
<td className="variant-comparison__value"><DisplayValue value={variant.value} search={search} /></td>
|
||||
<td>{highlighted(variant.typeLabel, search)}</td>
|
||||
<td><span>{variant.sources.length} {variant.sources.length === 1 ? 'source' : 'sources'}</span>
|
||||
<SourceList sources={variant.sources} search={search} /></td>
|
||||
</tr>)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<TechnicalSources comparison={comparison} search={search} />
|
||||
</section>;
|
||||
}
|
||||
|
|
@ -130,9 +130,25 @@ const TOOL_MESSAGE_TITLE = 'Test result';
|
|||
*/
|
||||
export function resultMessageFor(record: FindingRecord, fallbackToolId?: string): string | undefined {
|
||||
const toolId = toolIdForRecord(record, fallbackToolId);
|
||||
const headers = toolProfileDefinitions.find(profile => profile.id === toolId)?.presentation.resultMessageHeaders;
|
||||
const presentation = toolProfileDefinitions.find(profile => profile.id === toolId)?.presentation;
|
||||
const headers = presentation?.resultMessageHeaders;
|
||||
if (!headers?.length) return undefined;
|
||||
return record.fields.find(field => field.value && headers.some(pattern => pattern.test(field.label.trim())))?.value;
|
||||
for (const header of headers) {
|
||||
const field = record.fields.find(field => field.value && header.test(field.label.trim()) && isMessageField(field, record, presentation));
|
||||
if (field) return field.value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isMessageField(field: RecordField, record: FindingRecord, presentation: (typeof toolProfileDefinitions)[number]['presentation'] | undefined): boolean {
|
||||
if (!presentation?.resultMessageHeaders?.some(pattern => pattern.test(field.label.trim()))) return false;
|
||||
const statuses = presentation.statusHeaders ?? [/^(?:status|evaluation|result)$/i];
|
||||
if (!statuses.some(pattern => pattern.test(field.label.trim()))) return true;
|
||||
if (!/^(?:pass(?:ed)?|fail(?:ed)?|investigate|skipped|not[ -]?run|error|unknown|manual)$/i.test(field.value.trim())) return true;
|
||||
// An overlapping label is a message only when a separate populated status
|
||||
// exists. Native reports keep Result as status and ResultDetail as evidence.
|
||||
return record.fields.some(other => other.value && statuses.some(pattern => pattern.test(other.label.trim()))
|
||||
&& !presentation.resultMessageHeaders?.some(pattern => pattern.test(other.label.trim())));
|
||||
}
|
||||
|
||||
/** Waehlt den sichtbaren Status ueber die geordnete Profilpraeferenz. */
|
||||
|
|
@ -144,7 +160,7 @@ export function resultStatusFieldFor(record: FindingRecord, fallbackToolId?: str
|
|||
const field = record.fields.find(candidate =>
|
||||
candidate.value
|
||||
&& header.test(candidate.label.trim())
|
||||
&& !presentation?.resultMessageHeaders?.some(pattern => pattern.test(candidate.label.trim())));
|
||||
&& !isMessageField(candidate, record, presentation));
|
||||
if (field) {
|
||||
const displayLabel = presentation?.statusDisplayRules
|
||||
?.find(rule => rule.patterns.some(pattern => pattern.test(field.value.trim())))?.label;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type WheelEvent as ReactWheelEvent } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent, type WheelEvent as ReactWheelEvent } from 'react';
|
||||
import { useApp } from '../../app/AppProvider';
|
||||
import { TileCard } from '../../components/TileCard/TileCard';
|
||||
import { FloatingResultsWindow } from '../linked-results/FloatingResultsWindow';
|
||||
|
|
@ -78,7 +78,9 @@ function fittedCameraFor(graphNodes: Array<{ x: number; y: number }>, width: num
|
|||
const scale = clamp(Math.min(width / (maxX - minX), height / (maxY - minY)) * 0.88, 0.12, 1.8);
|
||||
return { x: -((minX + maxX) / 2) * scale, y: -((minY + maxY) / 2) * scale, scale };
|
||||
}
|
||||
export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest, onPathSnapshotApplied }: ObsidianGraphViewProps) {
|
||||
export const ObsidianGraphView = memo(ObsidianGraphContent);
|
||||
|
||||
function ObsidianGraphContent({ active, onOpenFinding, pathSnapshotRequest, onPathSnapshotApplied }: ObsidianGraphViewProps) {
|
||||
const app = useApp();
|
||||
const { dataset, activeSheet, activeSheetId, activeProfile, titleColumn, reviews, actionOnly, facetFilters, pathFindingIds, search: workSearch, statusFilter, setFacetFilter, setSelectedRecord, setView, toggleWorkspace, updateReview } = app;
|
||||
const [graphData] = useState<MitreGraphData>(() => loadStoredMitreMatrix() ?? defaultGraphData);
|
||||
|
|
@ -147,6 +149,7 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
const labelOffsets = useMemo(() => {
|
||||
const placed: Array<{ left: number; right: number; top: number; bottom: number }> = [];
|
||||
const offsets: Record<string, number> = {};
|
||||
if (!showText) return offsets;
|
||||
const candidates = [0, -22, 22, -44, 44, -66, 66, -88, 88, -110, 110, -132, 132, -154, 154, -176, 176, -198, 198, -220, 220];
|
||||
const ordered = [...displayedNodes].sort((left, right) => Number(left.kind === 'finding') - Number(right.kind === 'finding') || left.y - right.y);
|
||||
for (const node of ordered) {
|
||||
|
|
@ -167,7 +170,7 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
placed.push({ left, right, top: anchorY + offset - 15, bottom: anchorY + offset + 5 });
|
||||
}
|
||||
return offsets;
|
||||
}, [camera.scale, camera.x, camera.y, dimensions.height, dimensions.width, displayedNodes]);
|
||||
}, [camera.scale, camera.x, camera.y, dimensions.height, dimensions.width, displayedNodes, showText]);
|
||||
const nodeLookup = useMemo(() => new Map(nodes.map(node => [node.id, node])), [nodes]);
|
||||
const findingLookup = useMemo(() => new Map(items.map(item => [item.id, item])), [items]);
|
||||
const cardPlacements = useMemo<GraphCardPlacement[]>(() => {
|
||||
|
|
@ -378,14 +381,18 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
|
||||
useEffect(() => {
|
||||
const surface = surfaceRef.current;
|
||||
if (!surface || typeof ResizeObserver === 'undefined') return;
|
||||
if (!active || !surface || typeof ResizeObserver === 'undefined') return;
|
||||
const observer = new ResizeObserver(entries => {
|
||||
const rect = entries[0]?.contentRect;
|
||||
if (rect) setDimensions({ width: Math.max(1, rect.width), height: Math.max(1, rect.height) });
|
||||
if (rect) setDimensions(current => {
|
||||
const width = Math.max(1, rect.width);
|
||||
const height = Math.max(1, rect.height);
|
||||
return current.width === width && current.height === height ? current : { width, height };
|
||||
});
|
||||
});
|
||||
observer.observe(surface);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
}, [active]);
|
||||
|
||||
const stopCameraAnimation = useCallback(() => {
|
||||
if (cameraAnimationFrame.current !== undefined) globalThis.cancelAnimationFrame(cameraAnimationFrame.current);
|
||||
|
|
@ -434,17 +441,17 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
setExtractedSelectionIds(current => current.filter(id => id === FILTERED_GRAPH_SELECTION_ID || graph.nodes.some(node => node.id === id)));
|
||||
}, [graphSignature]);
|
||||
useEffect(() => {
|
||||
if (pathSnapshotRequest) setQueuedPathSnapshot(pathSnapshotRequest);
|
||||
}, [pathSnapshotRequest]);
|
||||
if (active && pathSnapshotRequest) setQueuedPathSnapshot(pathSnapshotRequest);
|
||||
}, [active, pathSnapshotRequest]);
|
||||
useEffect(() => {
|
||||
if (!queuedPathSnapshot) return;
|
||||
if (!active || !queuedPathSnapshot) return;
|
||||
const availableNodeIds = new Set(graph.nodes.map(node => node.id));
|
||||
const restored = queuedPathSnapshot.path.activeNodeIds.filter(id => availableNodeIds.has(id));
|
||||
setPinnedNodeIds(restored);
|
||||
setSelectedNodeId(restored.at(-1) ?? '');
|
||||
setQueuedPathSnapshot(undefined);
|
||||
onPathSnapshotApplied?.();
|
||||
}, [graph.nodes, graphSignature, onPathSnapshotApplied, queuedPathSnapshot]);
|
||||
}, [active, graph.nodes, graphSignature, onPathSnapshotApplied, queuedPathSnapshot]);
|
||||
useEffect(() => {
|
||||
if (!active || lastFittedGraphSignature.current === graphSignature) return;
|
||||
const frame = globalThis.requestAnimationFrame(() => {
|
||||
|
|
@ -457,6 +464,7 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
return () => globalThis.cancelAnimationFrame(frame);
|
||||
}, [active, fitGraphTo, graphSignature]);
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const extractSignature = extractedSelectionIds.join('|');
|
||||
if (!extractSignature) {
|
||||
fittedExtractSignature.current = '';
|
||||
|
|
@ -466,13 +474,14 @@ export function ObsidianGraphView({ active, onOpenFinding, pathSnapshotRequest,
|
|||
fittedExtractSignature.current = extractSignature;
|
||||
const frame = globalThis.requestAnimationFrame(() => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height), 520));
|
||||
return () => globalThis.cancelAnimationFrame(frame);
|
||||
}, [animateCameraTo, dimensions.height, dimensions.width, displayedNodes, extractedSelectionIds]);
|
||||
}, [active, animateCameraTo, dimensions.height, dimensions.width, displayedNodes, extractedSelectionIds]);
|
||||
useEffect(() => {
|
||||
if (!resetViewRevision || handledResetViewRevision.current === resetViewRevision) return;
|
||||
if (!active || !resetViewRevision || handledResetViewRevision.current === resetViewRevision) return;
|
||||
handledResetViewRevision.current = resetViewRevision;
|
||||
const frame = globalThis.requestAnimationFrame(() => animateCameraTo(fittedCameraFor(displayedNodes, dimensions.width, dimensions.height)));
|
||||
return () => globalThis.cancelAnimationFrame(frame);
|
||||
}, [animateCameraTo, dimensions.height, dimensions.width, displayedNodes, resetViewRevision]);
|
||||
}, [active, animateCameraTo, dimensions.height, dimensions.width, displayedNodes, resetViewRevision]);
|
||||
useEffect(() => { if (!active) stopCameraAnimation(); }, [active, stopCameraAnimation]);
|
||||
useEffect(() => stopCameraAnimation, [stopCameraAnimation]);
|
||||
const linksFor = (item: FindingPathItem) => dataset && activeSheet ? [
|
||||
...linkResultSheets(item.record, dataset.sheets, activeSheetId),
|
||||
|
|
|
|||
|
|
@ -141,11 +141,17 @@ export function extractFindingMitreFocus(records: FindingRecord[], tacticOrder:
|
|||
export function buildFindingPathItems(records: FindingRecord[], data: MitreGraphData, titleColumn = '', fallbackToolId?: string): FindingPathItem[] {
|
||||
const tacticLookup = new Map(data.tacticOrder.map(tactic => [simplify(tactic), tactic]));
|
||||
const techniqueLookup = new Map(data.techniques.map(technique => [technique.id, technique]));
|
||||
const headerKinds = new Map<string, string>();
|
||||
const headerKind = (label: string) => {
|
||||
let kind = headerKinds.get(label);
|
||||
if (kind === undefined) { kind = mitreBadgeLabel(label); headerKinds.set(label, kind); }
|
||||
return kind;
|
||||
};
|
||||
|
||||
return records.map(record => {
|
||||
const mitreFields = record.fields.filter(field => mitreBadgeLabel(field.label) && field.value);
|
||||
const attackFields = mitreFields.filter(field => mitreBadgeLabel(field.label) === 'MITRE ATT&CK');
|
||||
const defenseFields = mitreFields.filter(field => mitreBadgeLabel(field.label) !== 'MITRE ATT&CK');
|
||||
const mitreFields = record.fields.filter(field => field.value && headerKind(field.label));
|
||||
const attackFields = mitreFields.filter(field => headerKind(field.label) === 'MITRE ATT&CK');
|
||||
const defenseFields = mitreFields.filter(field => headerKind(field.label) !== 'MITRE ATT&CK');
|
||||
const hasNativeMitreField = attackFields.length > 0;
|
||||
const techniqueIds = [...new Set(attackFields.flatMap(field => [...field.value.toUpperCase().matchAll(/\bT\d{4}(?:\.\d{3})?\b/g)].map(match => match[0])))];
|
||||
const tactics = new Set<string>();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } fro
|
|||
import type { Assessment, AssessmentProject, CanonicalAssessmentModel, ImportFileRecord, ScanRun } from '../../domain/canonical-model';
|
||||
import type { GpoBackupStatusRecord, ProjectAssessmentHistoryResponse } from '../../api/assessor-api-contract';
|
||||
import { AssessorApiClient } from '../../api/assessor-api-client';
|
||||
import { defaultSecurityScoringPolicy } from '../../domain/security-scoring';
|
||||
import { Dialog } from '../../components/Dialog/Dialog';
|
||||
import { OperationProgressDialog } from '../../components/OperationProgress/OperationProgress';
|
||||
import { FileImport } from '../file-import/FileImport';
|
||||
|
|
@ -438,6 +439,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
projectId: form.project.id,
|
||||
name: `Timeslot ${formatTenantDate(effectiveAt)}`,
|
||||
effectiveAt,
|
||||
scoringPolicy: defaultSecurityScoringPolicy(),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
|
@ -542,11 +544,15 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
setError(null);
|
||||
setSaveFeedback(null);
|
||||
try {
|
||||
await assessorApi.adoptScanRunReplacement(projectId, replacedReport.scanRunId, replacementReport.scanRunId);
|
||||
const adopted = await assessorApi.adoptScanRunReplacement(projectId, replacedReport.scanRunId, replacementReport.scanRunId);
|
||||
await loadProjects();
|
||||
if (activeProjectId === projectId) await onProjectDataChanged?.(projectId);
|
||||
globalThis.dispatchEvent(new CustomEvent('assessor:datahub-changed', { detail: { projectId } }));
|
||||
setSaveFeedback({ tone: 'success', message: `${replacementReport.tool.name} was adopted as the replacement scan. The previous scan remains auditably retained.` });
|
||||
const transfer = adopted.reimportReviewTransfer;
|
||||
const preservation = transfer.unresolved > 0
|
||||
? ` ${transfer.transferred} client records were transferred; ${transfer.unresolved} remain retained on the previous scan and require review.`
|
||||
: ` ${transfer.transferred} client records were transferred safely.`;
|
||||
setSaveFeedback({ tone: 'success', message: `${replacementReport.tool.name} was adopted as the replacement scan. The previous scan remains auditably retained.${preservation}` });
|
||||
} catch (reason) {
|
||||
setError(reason instanceof Error ? reason.message : 'The imported scan could not be adopted as the replacement.');
|
||||
} finally {
|
||||
|
|
@ -652,6 +658,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
const processingByRun = new Map(reports.map(report => [report.scanRunId, summarizeScanProcessingReadiness(report.files)]));
|
||||
const affectedReports = reports.filter(report => processingByRun.get(report.scanRunId)?.state !== 'ready');
|
||||
const hasProcessingError = affectedReports.some(report => processingByRun.get(report.scanRunId)?.state === 'error');
|
||||
const hasProcessingReview = affectedReports.some(report => processingByRun.get(report.scanRunId)?.state === 'review');
|
||||
const replacementByRun = new Map(affectedReports.flatMap(affected => {
|
||||
const candidate = reports
|
||||
.filter(report => report.scanRunId !== affected.scanRunId
|
||||
|
|
@ -693,6 +700,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
const processing = processingByRun.get(report.scanRunId)!;
|
||||
const viewable = isDatahubScanViewable(readiness);
|
||||
const statusLabel = processing.state === 'update' ? 'Reimport recommended'
|
||||
: processing.state === 'review' ? 'Review required'
|
||||
: processing.state === 'error' ? 'Processing error'
|
||||
: readiness === 'recognized' ? 'Ready'
|
||||
: readiness === 'generic' ? 'Generic'
|
||||
|
|
@ -709,7 +717,7 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
<input type="checkbox" disabled={!viewable} checked={viewable && selected && !excludedScanRunIds.includes(report.scanRunId)} aria-label={`Select ${report.tool.name} scan report`} onChange={event => toggleScanReport(group.id, report.scanRunId, event.target.checked)}/>
|
||||
<span className="tenant-report-copy"><b>{report.tool.name}</b><small>{report.fileCount.toLocaleString('de-DE')} {report.fileCount === 1 ? 'file' : 'files'} · {report.formats.join(', ') || '—'}</small></span>
|
||||
<span className="tenant-report-imported">{formatHistoryDate(report.importedAt)}</span>
|
||||
<b className="tenant-report-status" data-status={processing.state === 'update' ? 'reimport-recommended' : processing.state === 'error' ? 'processing-error' : readiness} title={statusTitle}>{statusLabel}</b>
|
||||
<b className="tenant-report-status" data-status={processing.state === 'update' ? 'reimport-recommended' : processing.state === 'review' ? 'review-required' : processing.state === 'error' ? 'processing-error' : readiness} title={statusTitle}>{statusLabel}</b>
|
||||
<button className="btn tenant-scan-delete" type="button" title={`Delete ${report.tool.name} scan report`} aria-label={`Delete ${report.tool.name} scan report`} onClick={() => setScanDeleteTarget(report)}>Delete</button>
|
||||
</div>;
|
||||
})}
|
||||
|
|
@ -725,9 +733,10 @@ export function DataHub({ activeProjectId, onProjectSelected, onProjectDataChang
|
|||
</span>
|
||||
</div>)}
|
||||
</div>}
|
||||
{affectedReports.length > 0 && <section className="tenant-processing-alert" data-status={hasProcessingError ? 'error' : 'update'} aria-label={hasProcessingError ? 'ASSESSOR processing errors require attention' : 'ASSESSOR processing profile updates available'}>
|
||||
<header><strong>{hasProcessingError ? 'ASSESSOR processing issue' : 'New ASSESSOR processing profile available'}</strong><span>{hasProcessingError
|
||||
{affectedReports.length > 0 && <section className="tenant-processing-alert" data-status={hasProcessingError ? 'error' : hasProcessingReview ? 'review' : 'update'} aria-label={hasProcessingError ? 'ASSESSOR processing errors require attention' : hasProcessingReview ? 'ASSESSOR review required' : 'ASSESSOR processing profile updates available'}>
|
||||
<header><strong>{hasProcessingError ? 'ASSESSOR processing issue' : hasProcessingReview ? 'Review required' : 'New ASSESSOR processing profile available'}</strong><span>{hasProcessingError
|
||||
? 'At least one stored processing contract cannot be assessed safely. Review the affected scan and reimport its unchanged original file.'
|
||||
: hasProcessingReview ? 'The report needs attention because its scanner profile is generic or client-owned finding data could not be matched uniquely during reimport. Original data remains retained.'
|
||||
: 'ASSESSOR has updated its internal scanner-processing profile. Reimport the unchanged original scanner file to apply it; the existing scan remains active until replacement succeeds.'}</span></header>
|
||||
{affectedReports.map(report => {
|
||||
const replacement = replacementByRun.get(report.scanRunId);
|
||||
|
|
|
|||
|
|
@ -108,11 +108,11 @@ export function DatahubHistoryOverview({
|
|||
<span className="datahub-tenant-summary"><strong>{groups.length.toLocaleString('de-DE')}</strong><small>{groups.length === 1 ? 'Timeslot' : 'Timeslots'}</small></span>
|
||||
<span className="datahub-tenant-latest">{latest ? `Latest ${formatDate(latest.fileDate)}` : 'No scan data yet'}</span>
|
||||
</button>
|
||||
{(selected || processingSummary.state === 'update' || processingSummary.state === 'error') && <button
|
||||
{(selected || processingSummary.state === 'update' || processingSummary.state === 'review' || processingSummary.state === 'error') && <button
|
||||
className="btn datahub-tenant-show"
|
||||
data-status={processingSummary.state}
|
||||
type="button"
|
||||
title={processingSummary.state === 'update' || processingSummary.state === 'error' ? processingSummary.label : 'Manage client'}
|
||||
title={['update', 'review', 'error'].includes(processingSummary.state) ? processingSummary.label : 'Manage client'}
|
||||
onClick={() => onShow(project)}
|
||||
>Manage</button>}
|
||||
<button className="btn primary datahub-tenant-open" type="button" disabled={!latestOpenable} title={latestOpenable ? 'Open latest timeslot' : 'No scan data available'} onClick={() => latestOpenable && onOpen(project, latestOpenable.scanRunIds, latestOpenable.assessmentIds)}>Open</button>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { stableSheetId } from '../../shared/identifiers';
|
|||
import { groupProjectImportFiles } from './datahub-import-groups';
|
||||
import type { CveAffectedConfiguration, CveConfigurationMatch, CveEnrichmentRecord } from '../../domain/cve-enrichment';
|
||||
import { cveIdFromRuleId } from '../../domain/cve-enrichment';
|
||||
import { canonicalSecurityScores } from '../../scoring/canonical-security-score';
|
||||
import { defaultSecurityScoringPolicy } from '../../domain/security-scoring';
|
||||
|
||||
function text(value: JsonValue): string {
|
||||
if (value === null) return '';
|
||||
|
|
@ -215,7 +217,9 @@ function bindCanonicalFindings(sheet: DatasetSheet, occurrences: FindingOccurren
|
|||
return occurrence ? {
|
||||
...record,
|
||||
findingOccurrenceId: occurrence.id,
|
||||
findingDefinitionId: occurrence.definitionId
|
||||
findingDefinitionId: occurrence.definitionId,
|
||||
scanRunId: occurrence.scanRunId,
|
||||
importFileId: occurrence.importFileId
|
||||
} : record;
|
||||
})
|
||||
}
|
||||
|
|
@ -306,7 +310,8 @@ export function canonicalModelsToDataset(models: CanonicalAssessmentModel[]): Im
|
|||
const files = models.flatMap(model => model.importFiles);
|
||||
const tables = models.flatMap(model => model.sourceTables);
|
||||
const occurrences = models.flatMap(model => model.findingOccurrences);
|
||||
if (!runs.length || (!occurrences.length && !tables.some(table => table.rows?.length))) return null;
|
||||
const hasEnvironmentObservations = models.some(model => model.environmentObservations?.length);
|
||||
if (!runs.length || (!occurrences.length && !tables.some(table => table.rows?.length) && !hasEnvironmentObservations)) return null;
|
||||
|
||||
const fileById = new Map(files.map(file => [file.id, file]));
|
||||
const runById = new Map(runs.map(run => [run.id, run]));
|
||||
|
|
@ -396,12 +401,13 @@ export function canonicalModelsToDataset(models: CanonicalAssessmentModel[]): Im
|
|||
|
||||
const tool = projectTool(models);
|
||||
const aggregateName = project.name + ' · All Records';
|
||||
const aggregate = occurrences.length
|
||||
const aggregate = occurrences.length || hasEnvironmentObservations
|
||||
? bindCanonicalFindings(
|
||||
makeSheet(aggregateName, aggregateRows, rowsToCsv(aggregateRows), project.name, { isProjectSheet: true }),
|
||||
occurrences
|
||||
)
|
||||
: undefined;
|
||||
if (aggregate && !occurrences.length) aggregate.analysis.records = [];
|
||||
const lastModified = Math.max(...files.map(file => Date.parse(file.lastModifiedAt ?? file.importedAt)), 0);
|
||||
const sectionSheets = linkedSectionSheets(occurrences);
|
||||
const profileVersions = new Set(files.map(file => file.processedWith.profileVersion).filter(Boolean));
|
||||
|
|
@ -415,7 +421,8 @@ export function canonicalModelsToDataset(models: CanonicalAssessmentModel[]): Im
|
|||
sheets: [...(aggregate ? [aggregate] : []), ...tableSheets, ...supportingSheets, ...sectionSheets],
|
||||
profileId: tool.id === 'unknown' || tool.id === 'multiple' ? 'generic' : tool.id,
|
||||
profileVersion: profileVersions.size === 1 ? [...profileVersions][0] : profileVersions.size > 1 ? 'gemischt' : undefined,
|
||||
scanTool: tool.name
|
||||
scanTool: tool.name,
|
||||
securityScoring: { policy: models[0].assessment.scoringPolicy ?? defaultSecurityScoringPolicy(), scans: canonicalSecurityScores(models) }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { ImportFileRecord } from '../../domain/canonical-model';
|
|||
import type { GpoBackupStatusRecord } from '../../api/assessor-api-contract';
|
||||
import { currentProcessingManifest, processingReadiness, type ProcessingReadinessStatus } from '../../domain/processing-compatibility';
|
||||
|
||||
export type DatahubClientProcessingState = 'ready' | 'processing' | 'update' | 'error' | 'no-data';
|
||||
export type DatahubClientProcessingState = 'ready' | 'processing' | 'update' | 'review' | 'error' | 'no-data';
|
||||
|
||||
export interface DatahubClientProcessingSummary {
|
||||
state: DatahubClientProcessingState;
|
||||
|
|
@ -14,7 +14,7 @@ const ERROR_STATES = new Set(['incompatible', 'unknown']);
|
|||
const ATTENTION_STATES = new Set([...UPDATE_STATES, ...ERROR_STATES]);
|
||||
|
||||
export interface DatahubScanProcessingSummary {
|
||||
state: 'ready' | 'update' | 'error';
|
||||
state: 'ready' | 'update' | 'review' | 'error';
|
||||
statuses: ProcessingReadinessStatus[];
|
||||
explanation: string;
|
||||
}
|
||||
|
|
@ -24,6 +24,19 @@ export function summarizeScanProcessingReadiness(files: ImportFileRecord[]): Dat
|
|||
file.detectedTool.id || file.processedWith.profileId,
|
||||
file.processedWith
|
||||
)))];
|
||||
const unresolved = files.reduce((sum, file) => sum + (file.reimportReviewTransfer?.unresolved ?? 0), 0);
|
||||
if (unresolved > 0) {
|
||||
return {
|
||||
state: 'review', statuses,
|
||||
explanation: `${unresolved} client-owned finding ${unresolved === 1 ? 'record could' : 'records could'} not be matched uniquely during reimport. The original data is retained and requires review.`
|
||||
};
|
||||
}
|
||||
if (files.some(file => ['generic', 'tabular', 'multiple'].includes(file.detectedTool.id.trim().toLocaleLowerCase('en-US')))) {
|
||||
return {
|
||||
state: 'review', statuses,
|
||||
explanation: 'No scanner-specific processing profile was recognized. Keep the original source and reimport it after the scanner profile or mapping has been added or corrected.'
|
||||
};
|
||||
}
|
||||
if (!statuses.some(status => ATTENTION_STATES.has(status))) {
|
||||
return { state: 'ready', statuses, explanation: 'The scan uses a current or compatible ASSESSOR processing profile.' };
|
||||
}
|
||||
|
|
@ -68,16 +81,20 @@ export function summarizeClientProcessingReadiness(
|
|||
const runIds = new Set(activeScanRunIds);
|
||||
const statuses = files
|
||||
.filter(file => runIds.has(file.scanRunId))
|
||||
.map(file => processingReadiness(file.detectedTool.id || file.processedWith.profileId, file.processedWith));
|
||||
.map(file => summarizeScanProcessingReadiness([file]));
|
||||
|
||||
if (statuses.some(status => ERROR_STATES.has(status))) {
|
||||
if (statuses.some(status => status.state === 'error')) {
|
||||
return { state: 'error', label: 'Processing error requires attention in a client timeslot.' };
|
||||
}
|
||||
|
||||
if (statuses.some(status => UPDATE_STATES.has(status))) {
|
||||
if (statuses.some(status => status.state === 'update')) {
|
||||
return { state: 'update', label: 'ASSESSOR processing profile update available in a client timeslot.' };
|
||||
}
|
||||
|
||||
if (statuses.some(status => status.state === 'review')) {
|
||||
return { state: 'review', label: 'A client timeslot requires review or a future reimport.' };
|
||||
}
|
||||
|
||||
if (gpoBackups.some(record => record.state === 'queued' || record.state === 'running')) {
|
||||
return { state: 'processing', label: 'GPO processing in progress.' };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import type { Assessment, CanonicalAssessmentModel } from '../../domain/canonical-model';
|
||||
import type { ImportedDataset } from '../../domain/dataset';
|
||||
import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../domain/policy-evaluation';
|
||||
import type { UnifiedResultQuery, UnifiedResultQueryResult } from '../../domain/unified-result';
|
||||
import type { StoredPolicyEvaluation } from '../../domain/policy-evaluation';
|
||||
import { selectCurrentPolicyEvaluations, type UnifiedResultQuery, type UnifiedResultQueryResult } from '../../domain/unified-result';
|
||||
import type { PolicyAnalysisOverview } from '../../domain/policy-analysis-overview';
|
||||
import { queryAllUnifiedResults } from '../../exports/unified-result-jsonl';
|
||||
import { canonicalModelsToDataset } from './canonical-model-to-dataset';
|
||||
import { appendPolicyObservationsToDataset } from './policy-observations-to-dataset';
|
||||
import type { StoredMicrosoftSecurityBaselineAnalysis } from '../../domain/microsoft-security-baseline-analysis';
|
||||
import { buildPolicyGuidance } from '../../profiles/group-policy/guidance';
|
||||
import { canonicalSecurityScores } from '../../scoring/canonical-security-score';
|
||||
import { defaultSecurityScoringPolicy, type SecurityScanScore } from '../../domain/security-scoring';
|
||||
|
||||
export interface CanonicalProjectReader {
|
||||
listProjectAssessments(projectId: string): Promise<Assessment[]>;
|
||||
loadAssessmentModel(assessmentId: string): Promise<CanonicalAssessmentModel>;
|
||||
queryUnifiedResults?(query?: UnifiedResultQuery): Promise<UnifiedResultQueryResult>;
|
||||
listPolicyEvaluations?(projectId: string): Promise<{ records: StoredPolicyEvaluation[] }>;
|
||||
getPolicyAnalysisOverview?(projectId: string): Promise<Pick<PolicyAnalysisOverview, 'current'>>;
|
||||
listMicrosoftSecurityBaselineAnalyses?(baselineId: string): Promise<{ records: StoredMicrosoftSecurityBaselineAnalysis[] }>;
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +59,9 @@ export function canonicalModelForScanRuns(
|
|||
: occurrenceIds.has(review.target.occurrenceId)),
|
||||
diagnostics: model.diagnostics.filter(diagnostic =>
|
||||
fileIds.has(diagnostic.importFileId)
|
||||
&& (diagnostic.sourceTableId === undefined || tableIds.has(diagnostic.sourceTableId)))
|
||||
&& (diagnostic.sourceTableId === undefined || tableIds.has(diagnostic.sourceTableId))),
|
||||
environmentObservations: model.environmentObservations?.filter(item => selectedIds.has(item.scanRunId) && fileIds.has(item.importFileId)),
|
||||
reportSections: model.reportSections?.filter(item => selectedIds.has(item.scanRunId) && fileIds.has(item.importFileId))
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -65,25 +71,18 @@ export function canonicalModelForScanRun(model: CanonicalAssessmentModel, scanRu
|
|||
|
||||
export function currentPolicyEvaluationsForWork(
|
||||
records: StoredPolicyEvaluation[],
|
||||
observationIds: Set<string>
|
||||
observationIds: Set<string>,
|
||||
currentEvaluationIds?: ReadonlySet<string>
|
||||
): StoredPolicyEvaluation[] {
|
||||
const candidates = records.filter(record =>
|
||||
record.run.evaluator.id === POLICY_EVALUATION_ENGINE.id
|
||||
&& record.run.evaluator.version === POLICY_EVALUATION_ENGINE.version
|
||||
// The backend owns profile/identity/snapshot supersession. Result coverage
|
||||
// (in particular the referenced backup subset) is not a source identity.
|
||||
const allowedAggregations = currentEvaluationIds === undefined ? undefined : new Set(records
|
||||
.filter(record => currentEvaluationIds.has(record.run.id)).map(record => record.run.aggregationRunId));
|
||||
return selectCurrentPolicyEvaluations(records.filter(record =>
|
||||
(allowedAggregations === undefined || allowedAggregations.has(record.run.aggregationRunId))
|
||||
&& record.results.some(result => result.observationIds.some(id => observationIds.has(id)))
|
||||
);
|
||||
const selected = new Map<string, StoredPolicyEvaluation>();
|
||||
for (const record of candidates) {
|
||||
const backupIds = [...new Set(record.results.flatMap(result => result.backupInstanceIds))].sort();
|
||||
const sourceKey = backupIds.length ? backupIds.join('\u001f') : record.run.aggregationRunId;
|
||||
const current = selected.get(sourceKey);
|
||||
const shouldReplace = !current
|
||||
|| record.run.baselineAnalysisIds.length > current.run.baselineAnalysisIds.length
|
||||
|| (record.run.baselineAnalysisIds.length === current.run.baselineAnalysisIds.length
|
||||
&& `${record.run.completedAt}\u001f${record.run.id}` > `${current.run.completedAt}\u001f${current.run.id}`);
|
||||
if (shouldReplace) selected.set(sourceKey, record);
|
||||
}
|
||||
return [...selected.values()].sort((left, right) => left.run.completedAt.localeCompare(right.run.completedAt));
|
||||
&& record.results.every(result => result.observationIds.every(id => observationIds.has(id)))
|
||||
), { allowPreviousVersionForDisplay: true });
|
||||
}
|
||||
|
||||
export async function loadCanonicalProjectWorkspace(
|
||||
|
|
@ -109,12 +108,36 @@ export async function loadCanonicalProjectWorkspace(
|
|||
)
|
||||
: [];
|
||||
const scannerDataset = canonicalModelsToDataset(models);
|
||||
const evaluationList = policyObservations.length && reader.listPolicyEvaluations
|
||||
? await reader.listPolicyEvaluations(projectId)
|
||||
: undefined;
|
||||
if (scannerDataset?.securityScoring) {
|
||||
const currentIds = new Set(assessmentIds);
|
||||
const currentDates = models.map(model => Date.parse(model.assessment.effectiveAt ?? model.assessment.createdAt)).filter(Number.isFinite);
|
||||
const cutoff = currentDates.length ? Math.min(...currentDates) : undefined;
|
||||
const historical = cutoff === undefined ? [] : assessments.filter(item => !currentIds.has(item.id)
|
||||
&& Date.parse(item.effectiveAt ?? item.createdAt) < cutoff);
|
||||
const loadedById = new Map(loadedModels.map(model => [model.assessment.id, model]));
|
||||
const lastKnown = new Map<string, SecurityScanScore>();
|
||||
// Workspace-owned, read-only projection. Missing later scans retain their last
|
||||
// known open obligations; historical scores are never relabelled as current.
|
||||
for (const assessment of historical.sort((a, b) => Date.parse(a.effectiveAt ?? a.createdAt) - Date.parse(b.effectiveAt ?? b.createdAt))) {
|
||||
try {
|
||||
const historicModel = loadedById.get(assessment.id) ?? await reader.loadAssessmentModel(assessment.id);
|
||||
for (const scan of canonicalSecurityScores([historicModel])) {
|
||||
lastKnown.set(scan.comparisonKey ?? `unverified:${scan.toolId}:${scan.contentSha256 ?? scan.id}`, scan);
|
||||
}
|
||||
} catch { scannerDataset.securityScoring.historyUnavailable = true; }
|
||||
}
|
||||
if (historical.length) scannerDataset.securityScoring.baseline = [...lastKnown.values()];
|
||||
}
|
||||
const [evaluationList, policyOverview] = policyObservations.length && reader.listPolicyEvaluations
|
||||
? await Promise.all([
|
||||
reader.listPolicyEvaluations(projectId),
|
||||
reader.getPolicyAnalysisOverview?.(projectId)
|
||||
])
|
||||
: [];
|
||||
const evaluations = currentPolicyEvaluationsForWork(
|
||||
evaluationList?.records ?? [],
|
||||
new Set(policyObservations.map(result => result.sourceResultId))
|
||||
new Set(policyObservations.map(result => result.sourceResultId)),
|
||||
policyOverview ? new Set(policyOverview.current.evaluationRunIds) : undefined
|
||||
);
|
||||
const projectName = models[0]?.project.name ?? assessments.find(item => assessmentIds.includes(item.id))?.name ?? projectId;
|
||||
const guidance = buildPolicyGuidance(evaluations);
|
||||
|
|
@ -134,9 +157,13 @@ export async function loadCanonicalProjectWorkspace(
|
|||
} catch { /* Keep comparison/remediation; missing documentation is displayed explicitly. */ }
|
||||
}
|
||||
}
|
||||
const dataset = appendPolicyObservationsToDataset(scannerDataset, projectName, policyObservations, evaluations, guidance);
|
||||
if (dataset && !dataset.securityScoring) dataset.securityScoring = {
|
||||
policy: models[0]?.assessment.scoringPolicy ?? defaultSecurityScoringPolicy(), scans: []
|
||||
};
|
||||
return {
|
||||
models,
|
||||
assessmentIds,
|
||||
dataset: appendPolicyObservationsToDataset(scannerDataset, projectName, policyObservations, evaluations, guidance)
|
||||
dataset
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ImportedDataset } from '../../domain/dataset';
|
||||
import type { UnifiedResultProjection } from '../../domain/unified-result';
|
||||
import type { StoredPolicyEvaluation } from '../../domain/policy-evaluation';
|
||||
import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../domain/policy-evaluation';
|
||||
import { buildPolicyWorkResults } from '../../evaluation/policy-work-results';
|
||||
import { rowsToCsv } from '../../exports/csv-export';
|
||||
import { makeSheet } from '../../importers/sheet-factory';
|
||||
|
|
@ -23,13 +23,21 @@ export function appendPolicyObservationsToDataset(
|
|||
): ImportedDataset | null {
|
||||
const entries = buildPolicyWorkResults(results, evaluations);
|
||||
if (!entries.length) return dataset;
|
||||
const previousVersions = new Map(evaluations.filter(evaluation => evaluation.run.evaluator?.version
|
||||
&& evaluation.run.evaluator.version !== POLICY_EVALUATION_ENGINE.version)
|
||||
.flatMap(evaluation => evaluation.results.map(result => [result.id, evaluation.run.evaluator.version] as const)));
|
||||
const headers = [
|
||||
'Title', 'Category', 'Status', 'Scope', 'Configured Value', 'Baseline value', 'Baseline',
|
||||
'Comparison', 'Registry path', 'Sources', 'Technical Identity', 'Imported', 'Diagnostics', 'Policy type', 'Policy section',
|
||||
'Description', 'Remediation', 'Description source', 'Conflict', POLICY_CONFLICT_DETAILS, 'Duplicates', POLICY_DUPLICATE_DETAILS
|
||||
'Description', 'Remediation', 'Description source', 'Conflict', POLICY_CONFLICT_DETAILS, 'Duplicates', POLICY_DUPLICATE_DETAILS,
|
||||
'Evaluated', 'Source: Scan', 'Evaluation readiness'
|
||||
];
|
||||
const policyEntries = entries.map(entry => {
|
||||
const comparison = entry.comparison;
|
||||
const previousVersion = comparison && previousVersions.get(comparison.id);
|
||||
const readiness = previousVersion
|
||||
? `Re-evaluation required: showing saved results from engine ${previousVersion}; current engine is ${POLICY_EVALUATION_ENGINE.version}.`
|
||||
: '';
|
||||
const conflict = buildPolicyConflictPresentation(entry);
|
||||
const duplicate = buildPolicyDuplicatePresentation(entry);
|
||||
const configured = comparison?.customerState === 'missing' ? 'Not configured'
|
||||
|
|
@ -45,7 +53,11 @@ export function appendPolicyObservationsToDataset(
|
|||
row: [
|
||||
entry.title, 'GPOs', entry.status, entry.scope, display(configured),
|
||||
display(comparison?.comparator.baseline), comparison?.baselineLabel ?? '',
|
||||
comparison?.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')
|
||||
comparison?.baselineTemplateSelection?.status === 'no-match'
|
||||
? 'No baseline template matches the specified comparison context'
|
||||
: comparison?.diagnosticCodes.includes('policy-evaluation-template-context-required')
|
||||
? 'Different template recommendations; customer target role not established'
|
||||
: comparison?.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')
|
||||
? 'Conflicting baseline recommendations'
|
||||
: entry.status === 'Conflicts' ? 'Different values or types for the same setting'
|
||||
: entry.status === 'Duplicates' ? 'Identical setting in multiple source entries'
|
||||
|
|
@ -57,11 +69,12 @@ export function appendPolicyObservationsToDataset(
|
|||
[...new Set([...(comparison?.diagnosticCodes ?? []),
|
||||
...entry.sources.flatMap(source => source.policyDiagnosticCodes ?? [])])].join(', '),
|
||||
target?.kind ?? entry.sources[0]?.policyDeliveryMechanism ?? '', target && 'section' in target ? target.section : '',
|
||||
comparison ? guidance.get(comparison.id)?.description || 'Not available in imported baseline metadata.' : '',
|
||||
comparison ? [readiness, guidance.get(comparison.id)?.description || 'Not available in imported baseline metadata.'].filter(Boolean).join('\n\n') : '',
|
||||
comparison ? guidance.get(comparison.id)?.remediation ?? '' : '',
|
||||
comparison ? guidance.get(comparison.id)?.source ?? '' : '',
|
||||
conflict?.summary ?? '', conflict ? serializePolicyConflictPresentation(conflict) : '',
|
||||
duplicate?.summary ?? '', duplicate ? serializePolicyConflictPresentation(duplicate) : ''
|
||||
duplicate?.summary ?? '', duplicate ? serializePolicyConflictPresentation(duplicate) : '',
|
||||
entry.evaluatedAt ?? '', entry.importedAt ? `GPOs · ${entry.importedAt}` : 'GPOs', readiness
|
||||
]
|
||||
};
|
||||
});
|
||||
|
|
@ -76,7 +89,7 @@ export function appendPolicyObservationsToDataset(
|
|||
);
|
||||
if (!dataset) {
|
||||
const lastModified = Math.max(
|
||||
...policyEntries.map(entry => Date.parse(entry.importedAt)),
|
||||
...policyEntries.map(entry => Date.parse(entry.importedAt)).filter(Number.isFinite),
|
||||
0
|
||||
);
|
||||
return {
|
||||
|
|
@ -87,7 +100,7 @@ export function appendPolicyObservationsToDataset(
|
|||
rawText: '',
|
||||
sheets: [sheet],
|
||||
profileId: 'generic',
|
||||
profileVersion: 'gpo-work-comparison-1.2.0',
|
||||
profileVersion: 'gpo-work-comparison-1.2.1',
|
||||
scanTool: 'GPOs'
|
||||
};
|
||||
}
|
||||
|
|
@ -100,20 +113,26 @@ export function appendPolicyObservationsToDataset(
|
|||
const source = new Map(sourceHeaders.map((header, index) => [header, row[index] ?? '']));
|
||||
return aggregateHeaders.map(header => source.get(header) ?? '');
|
||||
});
|
||||
const aggregatePolicyRows = policyRows.map((row, rowIndex) => {
|
||||
const aggregatePolicyRows = policyRows.map(row => {
|
||||
const source = new Map(headers.map((header, index) => [header, row[index] ?? '']));
|
||||
source.set('Source: File', projectName);
|
||||
source.set('Source: Sheet', 'GPOs');
|
||||
source.set('Source: Scan', `GPOs · ${policyEntries[rowIndex]?.importedAt ?? ''}`);
|
||||
return aggregateHeaders.map(header => source.get(header) ?? '');
|
||||
});
|
||||
const aggregateRows = [aggregateHeaders, ...projectRows, ...aggregatePolicyRows];
|
||||
return makeSheet(first.name, aggregateRows, rowsToCsv(aggregateRows), first.sourceFile ?? projectName, {
|
||||
const combined = makeSheet(first.name, aggregateRows, rowsToCsv(aggregateRows), first.sourceFile ?? projectName, {
|
||||
id: first.id,
|
||||
sourceFile: first.sourceFile,
|
||||
sourceSheet: first.sourceSheet,
|
||||
isProjectSheet: true
|
||||
});
|
||||
// Keep exact scanner provenance when policy rows extend the aggregate.
|
||||
combined.analysis.records = combined.analysis.records.map((record, index) => {
|
||||
const original = first.analysis.records[index];
|
||||
return original ? { ...record, findingOccurrenceId: original.findingOccurrenceId,
|
||||
findingDefinitionId: original.findingDefinitionId, scanRunId: original.scanRunId, importFileId: original.importFileId } : record;
|
||||
});
|
||||
return combined;
|
||||
})() : first;
|
||||
return {
|
||||
...dataset,
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ function ResultRow({ item, expanded, onToggle, onUpdated }: {
|
|||
<li>Source: {item.sourceProfileId ?? '–'}@{item.sourceProfileVersion ?? '–'}</li>
|
||||
<li>Processing: profile {item.sourceProfileVersion ?? '–'} · mapping {item.mappingRuleVersion ?? '–'} · normalization {item.normalizationRuleVersion ?? '–'} · presentation {item.presentationVersion ?? '–'}</li>
|
||||
<li>Compatibility: {item.processingCompatibilityKey ?? 'unknown'} · {processingStatusLabel(item.processingStatus)}</li>
|
||||
{item.policyClassification && <li>GPO classification: {policyClassificationLabel(item.policyClassification)}{item.policySourceCount ? ` · ${item.policySourceCount.toLocaleString('en-US')} sources` : ''}</li>}
|
||||
{item.processingStatus === 'reimport-recommended' && <li>Reprocessing: {item.reprocessingAvailable ? 'retained source available' : 'original source must be uploaded again'}</li>}
|
||||
{environment ? <>
|
||||
<li>Subject class: {item.environmentSubjectType ?? '–'}</li>
|
||||
|
|
@ -289,6 +290,12 @@ function ResultRow({ item, expanded, onToggle, onUpdated }: {
|
|||
</div>;
|
||||
}
|
||||
|
||||
function policyClassificationLabel(value: string): string {
|
||||
if (value === 'not-configured') return 'Not configured';
|
||||
if (value === 'review-required') return 'Review required';
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function displayEnvironmentValue(value: unknown): string {
|
||||
if (value === undefined) return '–';
|
||||
if (typeof value === 'string') return value || '–';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface ResultsFilterPanelProps {
|
|||
|
||||
type MultiSelectFacetId = 'sourceKind' | 'sourceTool' | 'severity' | 'outcome' | 'technicalExecutionState'
|
||||
| 'resultType' | 'processingStatus' | 'reviewDisposition' | 'lifecycleState'
|
||||
| 'environmentState' | 'environmentProperty' | 'environmentSubjectType';
|
||||
| 'environmentState' | 'environmentProperty' | 'environmentSubjectType' | 'policyClassification';
|
||||
|
||||
const QUERY_KEY_BY_FACET: Record<MultiSelectFacetId, keyof UnifiedResultQuery> = {
|
||||
sourceKind: 'sourceKind',
|
||||
|
|
@ -23,7 +23,8 @@ const QUERY_KEY_BY_FACET: Record<MultiSelectFacetId, keyof UnifiedResultQuery> =
|
|||
lifecycleState: 'lifecycleState',
|
||||
environmentState: 'environmentState',
|
||||
environmentProperty: 'environmentProperty',
|
||||
environmentSubjectType: 'environmentSubjectType'
|
||||
environmentSubjectType: 'environmentSubjectType',
|
||||
policyClassification: 'policyClassification'
|
||||
};
|
||||
|
||||
const READONLY_FACET_IDS = new Set(['reviewStatus', 'effectiveState']);
|
||||
|
|
@ -87,11 +88,13 @@ export function ResultsFilterPanel({ facets, selected, onChange, onReset }: Resu
|
|||
{facet.values.length === 0 ? <p className="results-filter-empty">No values</p> : <ul className="results-filter-options">
|
||||
{facet.values.map(value => {
|
||||
const checked = currentValues.includes(value.value);
|
||||
const nextValues = toggleValue(currentValues, value.value);
|
||||
return <li key={value.value}>
|
||||
<label>
|
||||
<input type="checkbox" checked={checked} onChange={() => onChange({
|
||||
...selected,
|
||||
[queryKey]: toggleValue(currentValues, value.value)
|
||||
...(facet.id === 'policyClassification' ? { deviation: undefined } : {}),
|
||||
[queryKey]: nextValues
|
||||
} as UnifiedResultQuery)}/>
|
||||
<span title={value.label}>{value.label}</span>
|
||||
<b>{value.count.toLocaleString('en-US')}</b>
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ function renderFacet(
|
|||
const selectedValues = selected?.values ?? [];
|
||||
const multipleSeverities = Boolean(facet.severityScale && selectedValues.length > 1);
|
||||
const selectValue = multipleSeverities ? MULTI_SEVERITY_VALUE : (selectedValues[0] ?? '');
|
||||
const sourceScan = /^(?:quelle|source):\s*scan$/i.test(facet.label.trim());
|
||||
return <select
|
||||
className={`select dynamic-facet-select${active ? ' is-filtered' : ''}`}
|
||||
className={`select dynamic-facet-select${sourceScan ? ' is-source-scan' : ''}${active ? ' is-filtered' : ''}`}
|
||||
key={facet.id}
|
||||
value={selectValue}
|
||||
onChange={event => onFilter(facet.id, event.target.value ? { values: [event.target.value] } : null)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { useApp } from '../../app/AppProvider';
|
||||
import { deriveDynamicFacets, recountFacetsAgainstActiveFilters } from './dynamic-facets';
|
||||
import { sourceScanNavigationSheet } from './dynamic-facets';
|
||||
import { DynamicFilterControls } from './DynamicFilterControls';
|
||||
import { filterAndSortRecords } from './filter-records';
|
||||
import { selectWorkFacets } from './work-facets';
|
||||
import { measureNavigationStep } from '../../app/navigation-diagnostics';
|
||||
export { WorkTimeslotControl } from './WorkTimeslotControl';
|
||||
|
||||
export function SheetAndSearchControls() {
|
||||
|
|
@ -13,11 +15,22 @@ export function SheetAndSearchControls() {
|
|||
setActiveSheet,
|
||||
status
|
||||
} = app;
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
const selectedSheet = dataset?.sheets.find(sheet => sheet.id === activeSheetId) ?? dataset?.sheets[0];
|
||||
const availableSheets = optionsOpen ? dataset?.sheets : selectedSheet ? [selectedSheet] : [];
|
||||
// A workspace can contain thousands of evidence sheets. Native options are
|
||||
// needed when this control is used, not on every area mount. Flush before
|
||||
// the browser opens its native popup, for both pointer and keyboard focus.
|
||||
const prepareOptions = () => {
|
||||
if (!optionsOpen) flushSync(() => setOptionsOpen(true));
|
||||
};
|
||||
|
||||
return (
|
||||
<select className="select sheet-select" value={activeSheetId} disabled={!dataset?.sheets.length} onChange={event => setActiveSheet(event.target.value)}>
|
||||
<select className="select sheet-select" value={activeSheetId} disabled={!dataset?.sheets.length}
|
||||
onPointerDown={prepareOptions} onFocus={prepareOptions} onKeyDown={prepareOptions}
|
||||
onBlur={() => setOptionsOpen(false)} onChange={event => setActiveSheet(event.target.value)}>
|
||||
{!dataset?.sheets.length && <option value="">{status?.includes('Loading') ? 'Loading saved client …' : 'No file · 0'}</option>}
|
||||
{dataset?.sheets.map(sheet => <option key={sheet.id} value={sheet.id}>{sheet.name} · {sheet.analysis.records.length.toLocaleString('en-US')}</option>)}
|
||||
{availableSheets?.map(sheet => <option key={sheet.id} value={sheet.id}>{sheet.name} · {sheet.analysis.records.length.toLocaleString('en-US')}</option>)}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
|
@ -64,6 +77,7 @@ export function SearchFilterBar({ hiddenFacetLabels = [] }: SearchFilterBarProps
|
|||
search,
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
setActiveSheet,
|
||||
facetFilters,
|
||||
pathFindingIds,
|
||||
setFacetFilter,
|
||||
|
|
@ -71,33 +85,24 @@ export function SearchFilterBar({ hiddenFacetLabels = [] }: SearchFilterBarProps
|
|||
actionOnly
|
||||
} = app;
|
||||
|
||||
const dynamicFacets = useMemo(
|
||||
() => deriveDynamicFacets(activeSheet, dataset, activeProfile, reviews),
|
||||
[activeProfile, activeSheet, dataset, reviews]
|
||||
);
|
||||
const baseRecords = useMemo(
|
||||
() => filterAndSortRecords(activeSheet, {
|
||||
activeProfile, search, sortColumn: 'all', sortDirection: 'desc',
|
||||
statusFilter: 'all', facetFilters: {}, dataset, actionOnly, reviews, pathFindingIds
|
||||
}),
|
||||
[actionOnly, activeProfile, activeSheet, dataset, pathFindingIds, reviews, search]
|
||||
);
|
||||
const countedFacets = useMemo(
|
||||
() => recountFacetsAgainstActiveFilters({
|
||||
facets: dynamicFacets,
|
||||
records: baseRecords,
|
||||
filters: facetFilters,
|
||||
reviews,
|
||||
statusFilter,
|
||||
matchesStatus: (record, status) => activeProfile.matchesStatus(record, status)
|
||||
}),
|
||||
[activeProfile, baseRecords, dynamicFacets, facetFilters, reviews, statusFilter]
|
||||
() => measureNavigationStep('filter-counts', () => selectWorkFacets(activeSheet, {
|
||||
activeProfile, search, statusFilter, facetFilters, dataset, actionOnly, reviews, pathFindingIds
|
||||
})),
|
||||
[actionOnly, activeProfile, activeSheet, dataset, facetFilters, pathFindingIds, reviews, search, statusFilter]
|
||||
);
|
||||
const visibleFacets = useMemo(
|
||||
() => countedFacets.filter(facet => !hiddenFacetLabels.includes(facet.label)),
|
||||
[countedFacets, hiddenFacetLabels]
|
||||
);
|
||||
const defaultStatusFilter = activeProfile.defaults(activeSheet).statusFilter;
|
||||
const handleFacetFilter = useCallback<typeof setFacetFilter>((facetId, filter) => {
|
||||
if (facetId === 'provenance:source-scan' && filter?.values?.length) {
|
||||
const targetSheet = sourceScanNavigationSheet(dataset, activeSheet);
|
||||
if (targetSheet) setActiveSheet(targetSheet.id);
|
||||
}
|
||||
setFacetFilter(facetId, filter);
|
||||
}, [activeSheet, dataset, setActiveSheet, setFacetFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
countedFacets.forEach(facet => {
|
||||
|
|
@ -123,7 +128,7 @@ export function SearchFilterBar({ hiddenFacetLabels = [] }: SearchFilterBarProps
|
|||
statusFilter={statusFilter}
|
||||
defaultStatusFilter={defaultStatusFilter}
|
||||
onStatusFilter={setStatusFilter}
|
||||
onFilter={setFacetFilter}
|
||||
onFilter={handleFacetFilter}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { SEVERITY_LABEL, isSeverityHeader, normalizeSeverityValue, severityRank
|
|||
import { clean, normalizeNumericValue } from '../../shared/text';
|
||||
import { sourceFileLabel } from '../../shared/source-file-label';
|
||||
import { cisBenchmarkCategories } from '../../profiles/cis-cat/cis-benchmark-hierarchy';
|
||||
import { createSheetDerivationCache } from '../../shared/sheet-derivation-cache';
|
||||
|
||||
export type DynamicFacetKind = 'category' | 'number-range' | 'date-range';
|
||||
export type DynamicFacetSource = 'field' | 'profile-status' | 'review' | 'provenance' | 'derived';
|
||||
|
|
@ -45,6 +46,8 @@ export interface DynamicFacet {
|
|||
confidence?: RecognitionConfidenceLevel;
|
||||
derivedKind?: 'cis-benchmark-category';
|
||||
fallbackToolId?: string;
|
||||
/** Keep the declared GPO status vocabulary selectable even with zero hits. */
|
||||
preserveEmptyValues?: boolean;
|
||||
}
|
||||
|
||||
export interface DynamicFacetFilter {
|
||||
|
|
@ -197,7 +200,13 @@ function categoryIsUseful(column: ColumnProfile, values: string[], role?: Canoni
|
|||
const isPartialSingleCategory = role === 'category' && values.length > 0 && values.length < recordCount;
|
||||
if ((values.length < 2 && !isPartialSingleCategory)
|
||||
|| (column.type === 'Text' && role !== 'category') || column.type === 'leer') return false;
|
||||
const unique = new Set(values.map(normalized)).size;
|
||||
// Once the cardinality limit is exceeded, no later value can make this a facet.
|
||||
const distinct = new Set<string>();
|
||||
for (const value of values) {
|
||||
distinct.add(normalized(value));
|
||||
if (distinct.size > MAX_CATEGORY_VALUES) return false;
|
||||
}
|
||||
const unique = distinct.size;
|
||||
const averageLength = values.reduce((sum, value) => sum + value.length, 0) / values.length;
|
||||
// A single explicit category is still a useful selector when other rows are
|
||||
// uncategorized, for example GPO findings appended to scanner results.
|
||||
|
|
@ -266,6 +275,7 @@ function fieldFacets(sheet: DatasetSheet, dataset: ImportedDataset | null | unde
|
|||
siblingCounts.set(key, (siblingCounts.get(key) ?? 0) + 1);
|
||||
});
|
||||
const emittedGroups = new Set<string>();
|
||||
const groupValues = new Map<string, string[]>();
|
||||
|
||||
return sheet.analysis.columns.flatMap((column, columnIndex) => {
|
||||
const recognition = fields.find(field => field.columnIndex === columnIndex);
|
||||
|
|
@ -292,6 +302,7 @@ function fieldFacets(sheet: DatasetSheet, dataset: ImportedDataset | null | unde
|
|||
const facet = rangeFacet(column, columnIndex, role, recognition?.confidence.level, sheet.analysis.records);
|
||||
return facet ? [{ ...facet, priority: facetPriority(facet.label, role) }] : [];
|
||||
}
|
||||
if ((column.type === 'Text' && role !== 'category') || column.type === 'leer') return [];
|
||||
|
||||
const groupKey = siblingGroupKey(column.header);
|
||||
const isSiblingGroup = (siblingCounts.get(groupKey) ?? 0) > 1;
|
||||
|
|
@ -303,7 +314,17 @@ function fieldFacets(sheet: DatasetSheet, dataset: ImportedDataset | null | unde
|
|||
: [])
|
||||
: [columnIndex];
|
||||
|
||||
const rawValues = sheet.analysis.records.flatMap(record => recordValues(record, columnIndexes));
|
||||
const isScanSource = /^(?:quelle|source):\s*scan$/i.test(clean(column.header));
|
||||
// Built once from the complete workspace below. A sheet-local facet here
|
||||
// would hide all other scans while a dedicated GPO view is active.
|
||||
if (isScanSource) return [];
|
||||
// Rejected numbered groups used to rescan every sibling for every column.
|
||||
// Reuse their values as well, while retaining each column's role/type rules.
|
||||
let rawValues = isSiblingGroup ? groupValues.get(groupKey) : undefined;
|
||||
if (!rawValues) {
|
||||
rawValues = sheet.analysis.records.flatMap(record => recordValues(record, columnIndexes));
|
||||
if (isSiblingGroup) groupValues.set(groupKey, rawValues);
|
||||
}
|
||||
if (!categoryIsUseful(column, rawValues, role, sheet.analysis.records.length)) return [];
|
||||
if (isSiblingGroup) emittedGroups.add(groupKey);
|
||||
// Beim Zusammenfassen entfaellt die Positionsnummer aus der Beschriftung.
|
||||
|
|
@ -326,7 +347,7 @@ function fieldFacets(sheet: DatasetSheet, dataset: ImportedDataset | null | unde
|
|||
});
|
||||
}
|
||||
|
||||
function profileStatusFacet(sheet: DatasetSheet, profile: ImportProfile): DynamicFacet | null {
|
||||
function profileStatusFacet(sheet: DatasetSheet, profile: ImportProfile, preserveEmptyValues = false): DynamicFacet | null {
|
||||
const filters = profile.presentation.statusFilters.filter(filter => filter.value !== 'all');
|
||||
if (!filters.length) return null;
|
||||
return {
|
||||
|
|
@ -334,11 +355,12 @@ function profileStatusFacet(sheet: DatasetSheet, profile: ImportProfile): Dynami
|
|||
label: 'Result Status',
|
||||
kind: 'category',
|
||||
source: 'profile-status',
|
||||
preserveEmptyValues,
|
||||
values: filters.map(filter => ({
|
||||
value: filter.value,
|
||||
label: filter.label,
|
||||
count: sheet.analysis.records.filter(record => profile.matchesStatus(record, filter.value)).length
|
||||
})).filter(value => value.count > 0)
|
||||
})).filter(value => preserveEmptyValues || value.count > 0)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -358,19 +380,18 @@ interface CachedStaticFacets {
|
|||
profileFacet: DynamicFacet | null;
|
||||
cisCategories: DynamicFacet | null;
|
||||
fields: DynamicFacet[];
|
||||
sourceFiles: DynamicFacet | null;
|
||||
}
|
||||
|
||||
const staticFacetsCache = new WeakMap<DatasetSheet, CachedStaticFacets>();
|
||||
const staticFacetsCache = createSheetDerivationCache<CachedStaticFacets>();
|
||||
|
||||
function getStaticFacets(
|
||||
sheet: DatasetSheet,
|
||||
dataset: ImportedDataset | null | undefined,
|
||||
profile: ImportProfile
|
||||
): CachedStaticFacets {
|
||||
let cached = staticFacetsCache.get(sheet);
|
||||
if (!cached) {
|
||||
const profileFacet = profileStatusFacet(sheet, profile);
|
||||
return staticFacetsCache(sheet, [dataset, profile], () => {
|
||||
const profileFacet = profileStatusFacet(sheet, profile, sheet.toolId === 'group-policy'
|
||||
|| Boolean(dataset?.sheets?.some(item => item.toolId === 'group-policy')));
|
||||
const cisCategoryValues = sheet.analysis.records.flatMap(record => cisBenchmarkCategories(record, profile.id));
|
||||
const cisCategoryFacetValues = categoricalValues(cisCategoryValues);
|
||||
const cisCategories = cisCategoryFacetValues.length
|
||||
|
|
@ -387,30 +408,86 @@ function getStaticFacets(
|
|||
const fields = fieldFacets(sheet, dataset, Boolean(profileFacet))
|
||||
.sort((left, right) => right.priority - left.priority || left.label.localeCompare(right.label, 'en-US'))
|
||||
.map(({ priority: _priority, ...facet }) => facet);
|
||||
const hasScanFacet = fields.some(facet => /^(?:quelle|source):\s*scan$/i.test(clean(facet.label)));
|
||||
const sourceFiles = hasScanFacet
|
||||
? null
|
||||
: virtualFacet({
|
||||
id: 'provenance:source-file',
|
||||
label: 'Source File',
|
||||
source: 'provenance',
|
||||
values: sheet.analysis.records.map(record => sourceFileLabel(record.sourceFile)).filter(Boolean)
|
||||
});
|
||||
cached = { profileFacet, cisCategories, fields, sourceFiles };
|
||||
staticFacetsCache.set(sheet, cached);
|
||||
return { profileFacet, cisCategories, fields };
|
||||
});
|
||||
}
|
||||
|
||||
function recordSourceScan(record: FindingRecord): string {
|
||||
return clean(record.fields.find(field => /^(?:quelle|source):\s*scan$/i.test(clean(field.label)))?.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Herkunftswerte des workspaceweiten Blattes.
|
||||
*
|
||||
* Die Ermittlung liest je Datensatz jede Spaltenueberschrift und normalisiert
|
||||
* sie. Bei einem Mandantenblatt mit mehreren tausend Datensaetzen sind das
|
||||
* hunderttausend Zeichenkettenoperationen - zu teuer, um sie bei jedem
|
||||
* Ansichtswechsel und jeder Facettenableitung erneut zu durchlaufen. Die
|
||||
* Datensatzliste eines geladenen Blattes ist unveraenderlich, deshalb bleibt
|
||||
* das Ergebnis an ihr haengen.
|
||||
*/
|
||||
const sourceScanValuesCache = new WeakMap<readonly FindingRecord[], DynamicFacetValue[]>();
|
||||
|
||||
function workspaceSourceScanValues(records: FindingRecord[]): DynamicFacetValue[] {
|
||||
let cached = sourceScanValuesCache.get(records);
|
||||
if (!cached) {
|
||||
cached = categoricalValues(records.map(recordSourceScan).filter(Boolean));
|
||||
sourceScanValuesCache.set(records, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/** Source: Scan is workspace navigation, not a local sheet facet. */
|
||||
function workspaceSourceScanFacet(
|
||||
sheet: DatasetSheet,
|
||||
dataset: ImportedDataset | null | undefined,
|
||||
profile: ImportProfile
|
||||
): DynamicFacet | null {
|
||||
const aggregate = dataset?.sheets?.find(candidate => candidate.isProjectSheet && candidate.analysis.records.length);
|
||||
const records = aggregate?.analysis.records ?? sheet.analysis.records;
|
||||
const sourceValues = workspaceSourceScanValues(records);
|
||||
const fallbackScanLabel = clean(dataset?.scanTool || profile.name || sheet.toolId || '');
|
||||
const values = sourceValues.length
|
||||
? sourceValues
|
||||
: fallbackScanLabel
|
||||
? [{ value: normalized(fallbackScanLabel), label: fallbackScanLabel, count: records.length }]
|
||||
: [];
|
||||
return values.length ? {
|
||||
id: 'provenance:source-scan', label: 'Source: Scan', kind: 'category', source: 'provenance', values
|
||||
} : null;
|
||||
}
|
||||
|
||||
export function sourceScanNavigationSheet(
|
||||
dataset: ImportedDataset | null | undefined,
|
||||
activeSheet: DatasetSheet | undefined
|
||||
): DatasetSheet | undefined {
|
||||
if (!dataset || activeSheet?.isProjectSheet) return undefined;
|
||||
return dataset.sheets.find(sheet => sheet.isProjectSheet && sheet.analysis.records.length);
|
||||
}
|
||||
|
||||
const dynamicFacetsCache = createSheetDerivationCache<DynamicFacet[]>();
|
||||
const emptyFacets: DynamicFacet[] = [];
|
||||
|
||||
export function deriveDynamicFacets(
|
||||
sheet: DatasetSheet | undefined,
|
||||
dataset: ImportedDataset | null | undefined,
|
||||
profile: ImportProfile,
|
||||
reviews: Record<string, Review>
|
||||
): DynamicFacet[] {
|
||||
if (!sheet?.analysis.records.length) return emptyFacets;
|
||||
return dynamicFacetsCache(sheet, [dataset, profile, reviews], () => buildDynamicFacets(sheet, dataset, profile, reviews));
|
||||
}
|
||||
|
||||
function buildDynamicFacets(
|
||||
sheet: DatasetSheet | undefined,
|
||||
dataset: ImportedDataset | null | undefined,
|
||||
profile: ImportProfile,
|
||||
reviews: Record<string, Review>
|
||||
): DynamicFacet[] {
|
||||
if (!sheet?.analysis.records.length) return [];
|
||||
|
||||
const { profileFacet, cisCategories, fields, sourceFiles } = getStaticFacets(sheet, dataset, profile);
|
||||
const { profileFacet, cisCategories, fields } = getStaticFacets(sheet, dataset, profile);
|
||||
const sourceScan = workspaceSourceScanFacet(sheet, dataset, profile);
|
||||
|
||||
const reviewed = sheet.analysis.records.map(record => reviews[record.id]).filter((review): review is Review => Boolean(review));
|
||||
const reviewFacets = [
|
||||
|
|
@ -421,21 +498,15 @@ export function deriveDynamicFacets(
|
|||
|
||||
// Arbeitsreihenfolge: Ergebnisstatus und die fachlichen Achsen zuerst,
|
||||
// danach Review- und Herkunftsfacetten.
|
||||
const available = [profileFacet, cisCategories, ...fields, ...reviewFacets, sourceFiles]
|
||||
const available = [profileFacet, cisCategories, ...fields, ...reviewFacets, sourceScan]
|
||||
.filter((facet): facet is DynamicFacet => Boolean(facet));
|
||||
const visible = available.slice(0, MAX_VISIBLE_FACETS);
|
||||
const scanSource = available.find(facet => /^(?:quelle|source):\s*scan$/i.test(clean(facet.label)));
|
||||
if (scanSource && !visible.some(facet => facet.id === scanSource.id)) {
|
||||
// Die projektweite Werkzeugauswahl ist eine bindende Herkunftsfacette.
|
||||
// GPO-spezifische Zusatzspalten duerfen sie nicht aus der begrenzten
|
||||
// Filterleiste verdraengen. Sie bleibt als letzter sekundaerer Filter
|
||||
// sichtbar, ohne die vorderen fachlichen Arbeitsfilter umzuschichten.
|
||||
let replaceIndex = visible.length - 1;
|
||||
while (replaceIndex >= 0 && isPrimaryFacet(visible[replaceIndex])) replaceIndex -= 1;
|
||||
if (replaceIndex >= 0) visible.splice(replaceIndex, 1, scanSource);
|
||||
else if (visible.length) visible.splice(visible.length - 1, 1, scanSource);
|
||||
else visible.push(scanSource);
|
||||
}
|
||||
const maximum = sheet.toolId === 'group-policy' ? 11 : MAX_VISIBLE_FACETS;
|
||||
// Source: Scan is the permanent final provenance control. Keeping it last
|
||||
// lets the shared layout pin it to the right in every Work view.
|
||||
const visible = available.filter(facet => facet.id !== scanSource?.id)
|
||||
.slice(0, Math.max(0, maximum - (scanSource ? 1 : 0)));
|
||||
if (scanSource) visible.push(scanSource);
|
||||
return visible;
|
||||
}
|
||||
|
||||
|
|
@ -470,7 +541,8 @@ export function primaryFacetCount(facets: DynamicFacet[]): number {
|
|||
*
|
||||
* Der eigene Filter der Facette bleibt bewusst ausgenommen, damit weiterhin
|
||||
* zwischen den unter den uebrigen Filtern erreichbaren Alternativen gewechselt
|
||||
* werden kann. Werte ohne Treffer werden nicht angeboten.
|
||||
* werden kann. GPO-Statuswerte bleiben auch mit null Treffern auswaehlbar;
|
||||
* andere Werte ohne Treffer werden nicht angeboten.
|
||||
*/
|
||||
export function recountFacetsAgainstActiveFilters(options: {
|
||||
facets: DynamicFacet[];
|
||||
|
|
@ -480,42 +552,40 @@ export function recountFacetsAgainstActiveFilters(options: {
|
|||
statusFilter: string;
|
||||
matchesStatus(record: FindingRecord, status: string): boolean;
|
||||
}): DynamicFacet[] {
|
||||
return options.facets.map(facet => {
|
||||
const others = options.facets.filter(other => other.id !== facet.id);
|
||||
const base = options.records.filter(record => {
|
||||
if (facet.source !== 'profile-status' && !options.matchesStatus(record, options.statusFilter)) return false;
|
||||
return others.every(other => {
|
||||
const filter = options.filters[other.id];
|
||||
return !filter || matchesDynamicFacets(record, [other], { [other.id]: filter }, options.reviews);
|
||||
});
|
||||
});
|
||||
|
||||
if (facet.source === 'profile-status') {
|
||||
return { ...facet, values: facet.values
|
||||
.map(value => ({ ...value, count: base.filter(record => options.matchesStatus(record, value.value)).length }))
|
||||
.filter(value => value.count > 0) };
|
||||
const active = options.facets.filter(facet => options.filters[facet.id] && facet.source !== 'profile-status');
|
||||
const counted = options.facets.filter(facet => facet.kind === 'category' && facet.id !== 'provenance:source-scan');
|
||||
const counts = new Map(counted.map(facet => [facet.id, new Map<string, number>()]));
|
||||
// Evaluate active selections once per record. Self-exclusion permits at most
|
||||
// one failing facet, and only that facet may count the record as an alternative.
|
||||
for (const record of options.records) {
|
||||
const failures = active.filter(facet => !matchesDynamicFacets(record, [facet],
|
||||
{ [facet.id]: options.filters[facet.id] }, options.reviews));
|
||||
if (failures.length > 1) continue;
|
||||
const statusMatches = options.matchesStatus(record, options.statusFilter);
|
||||
for (const facet of counted) {
|
||||
if (failures.length && failures[0].id !== facet.id) continue;
|
||||
const values = facet.source === 'profile-status'
|
||||
? facet.values.filter(value => options.matchesStatus(record, value.value)).map(value => value.value)
|
||||
: statusMatches
|
||||
? [...new Set(valuesForFacet(record, facet, options.reviews).map(value => facetValueKey(facet.severityScale, value)))]
|
||||
: [];
|
||||
const bucket = counts.get(facet.id)!;
|
||||
for (const value of values) if (value) bucket.set(value, (bucket.get(value) ?? 0) + 1);
|
||||
}
|
||||
return facet.kind === 'category' ? { ...facet, values: recountCategory(facet, base, options.reviews) } : facet;
|
||||
}
|
||||
return options.facets.map(facet => {
|
||||
const bucket = counts.get(facet.id);
|
||||
return bucket ? { ...facet, values: facet.values
|
||||
.map(value => ({ ...value, count: bucket.get(value.value) ?? 0 }))
|
||||
.filter(value => facet.preserveEmptyValues || value.count > 0) } : facet;
|
||||
});
|
||||
}
|
||||
|
||||
function recountCategory(facet: DynamicFacet, records: FindingRecord[], reviews: Record<string, Review>): DynamicFacetValue[] {
|
||||
const counts = new Map<string, number>();
|
||||
records.forEach(record => {
|
||||
// Derselbe Schluessel wie beim Aufbau der Facette: sonst zaehlt die
|
||||
// Rohschreibweise gegen einen kanonischen Eintrag und jede Stufe zeigte 0.
|
||||
new Set(valuesForFacet(record, facet, reviews).map(value => facetValueKey(facet.severityScale, value))).forEach(value => {
|
||||
if (value) counts.set(value, (counts.get(value) ?? 0) + 1);
|
||||
});
|
||||
});
|
||||
return facet.values
|
||||
.map(value => ({ ...value, count: counts.get(value.value) ?? 0 }))
|
||||
.filter(value => value.count > 0);
|
||||
}
|
||||
|
||||
function valueForFacet(record: FindingRecord, facet: DynamicFacet, reviews: Record<string, Review>): string {
|
||||
if (facet.source === 'field' && facet.columnIndex !== undefined) return fieldValue(record, facet.columnIndex);
|
||||
if (facet.source === 'provenance') return sourceFileLabel(record.sourceFile);
|
||||
if (facet.source === 'provenance') return facet.id === 'provenance:source-scan'
|
||||
? recordSourceScan(record) || (facet.values.length === 1 ? facet.values[0]?.label ?? '' : '')
|
||||
: sourceFileLabel(record.sourceFile);
|
||||
if (facet.source === 'review' && facet.reviewField) return clean(reviews[record.id]?.[facet.reviewField]);
|
||||
return '';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { Review } from '../../domain/review';
|
|||
import { normalizeNumericValue, plainTextValue } from '../../shared/text';
|
||||
import { findSeverityHeaderIndex, normalizeSeverityValue, severityRank } from '../../domain/severity-order';
|
||||
import { deriveDynamicFacets, matchesDynamicFacets, type DynamicFacetFilters } from './dynamic-facets';
|
||||
import { createSheetDerivationCache } from '../../shared/sheet-derivation-cache';
|
||||
|
||||
export interface RecordFilterOptions {
|
||||
activeProfile: ImportProfile;
|
||||
|
|
@ -19,7 +20,22 @@ export interface RecordFilterOptions {
|
|||
reviews: Record<string, Review>;
|
||||
}
|
||||
|
||||
const cachedRecords = createSheetDerivationCache<FindingRecord[]>();
|
||||
const emptyRecords: FindingRecord[] = [];
|
||||
|
||||
export function recordFilterKeys(options: RecordFilterOptions): readonly unknown[] {
|
||||
return [options.activeProfile, options.dataset, options.reviews, options.search,
|
||||
options.sortColumn, options.sortDirection, options.statusFilter, options.actionOnly,
|
||||
options.pathFindingIds?.length ? options.pathFindingIds : undefined,
|
||||
JSON.stringify(options.facetFilters ?? {})];
|
||||
}
|
||||
|
||||
export function filterAndSortRecords(sheet: DatasetSheet | undefined, options: RecordFilterOptions): FindingRecord[] {
|
||||
if (!sheet) return emptyRecords;
|
||||
return cachedRecords(sheet, recordFilterKeys(options), () => deriveFilteredRecords(sheet, options));
|
||||
}
|
||||
|
||||
function deriveFilteredRecords(sheet: DatasetSheet, options: RecordFilterOptions): FindingRecord[] {
|
||||
if (!sheet) return [];
|
||||
const facets = deriveDynamicFacets(sheet, options.dataset, options.activeProfile, options.reviews);
|
||||
const pathFindingIds = options.pathFindingIds?.length ? new Set(options.pathFindingIds) : undefined;
|
||||
|
|
@ -37,17 +53,19 @@ export function filterAndSortRecords(sheet: DatasetSheet | undefined, options: R
|
|||
if (options.sortColumn === 'all') {
|
||||
const severityIndex = findSeverityHeaderIndex(sheet.analysis.headers);
|
||||
if (severityIndex < 0) return filtered;
|
||||
return [...filtered].sort((left, right) =>
|
||||
severityRank(normalizeSeverityValue(plainTextValue(left.fields[severityIndex]?.value)))
|
||||
- severityRank(normalizeSeverityValue(plainTextValue(right.fields[severityIndex]?.value)))
|
||||
const ranks = new Map(filtered.map(record => [record,
|
||||
severityRank(normalizeSeverityValue(plainTextValue(record.fields[severityIndex]?.value)))]));
|
||||
return filtered.sort((left, right) =>
|
||||
ranks.get(left)! - ranks.get(right)!
|
||||
|| left.index - right.index);
|
||||
}
|
||||
const index = sheet.analysis.headers.indexOf(options.sortColumn);
|
||||
if (index < 0) return filtered;
|
||||
const direction = options.sortDirection === 'asc' ? 1 : -1;
|
||||
return [...filtered].sort((left, right) => {
|
||||
const leftValue = normalizeNumericValue(left.fields[index]?.value);
|
||||
const rightValue = normalizeNumericValue(right.fields[index]?.value);
|
||||
const values = new Map(filtered.map(record => [record, normalizeNumericValue(record.fields[index]?.value)]));
|
||||
return filtered.sort((left, right) => {
|
||||
const leftValue = values.get(left)!;
|
||||
const rightValue = values.get(right)!;
|
||||
if (leftValue === null && rightValue === null) return left.index - right.index;
|
||||
if (leftValue === null) return 1;
|
||||
if (rightValue === null) return -1;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import type { DatasetSheet } from '../../domain/dataset';
|
||||
import { createSheetDerivationCache } from '../../shared/sheet-derivation-cache';
|
||||
import { deriveDynamicFacets, recountFacetsAgainstActiveFilters, type DynamicFacet } from './dynamic-facets';
|
||||
import { filterAndSortRecords, recordFilterKeys, type RecordFilterOptions } from './filter-records';
|
||||
|
||||
export type WorkFacetOptions = Omit<RecordFilterOptions, 'sortColumn' | 'sortDirection'>;
|
||||
const cache = createSheetDerivationCache<DynamicFacet[]>();
|
||||
const emptyFacets: DynamicFacet[] = [];
|
||||
const emptyFilters = {};
|
||||
|
||||
/** Dropdowns, sidebar and graph share one unsorted, self-excluding count projection. */
|
||||
export function selectWorkFacets(sheet: DatasetSheet | undefined, options: WorkFacetOptions): DynamicFacet[] {
|
||||
if (!sheet) return emptyFacets;
|
||||
const query: RecordFilterOptions = { ...options, sortColumn: '', sortDirection: 'asc' };
|
||||
return cache(sheet, recordFilterKeys(query), () => {
|
||||
const facets = deriveDynamicFacets(sheet, options.dataset, options.activeProfile, options.reviews);
|
||||
const records = filterAndSortRecords(sheet, { ...query, statusFilter: 'all', facetFilters: emptyFilters });
|
||||
return recountFacetsAgainstActiveFilters({ facets, records, filters: options.facetFilters ?? emptyFilters,
|
||||
reviews: options.reviews, statusFilter: options.statusFilter, matchesStatus: options.activeProfile.matchesStatus });
|
||||
});
|
||||
}
|
||||
|
|
@ -6,12 +6,9 @@ import type { DatasetSheet, ImportedDataset } from '../../domain/dataset';
|
|||
import type { ImportProfile } from '../../domain/import-profile';
|
||||
import type { Review } from '../../domain/review';
|
||||
import { SEVERITY_LABEL } from '../../domain/severity-order';
|
||||
import {
|
||||
deriveDynamicFacets,
|
||||
recountFacetsAgainstActiveFilters,
|
||||
type DynamicFacetFilters
|
||||
} from '../search-filter/dynamic-facets';
|
||||
import { filterAndSortRecords } from '../search-filter/filter-records';
|
||||
import { type DynamicFacetFilters } from '../search-filter/dynamic-facets';
|
||||
import { selectWorkFacets } from '../search-filter/work-facets';
|
||||
import { measureNavigationStep } from '../../app/navigation-diagnostics';
|
||||
|
||||
type StandardSeverity = typeof STANDARD_SEVERITIES[number];
|
||||
const STANDARD_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'] as const satisfies readonly CanonicalSeverity[];
|
||||
|
|
@ -57,32 +54,11 @@ export function summarizeSeverities(options: SeverityMetricOptions): SidebarSeve
|
|||
const { sheet, dataset, activeProfile, reviews, statusFilter, facetFilters, pathFindingIds, search, actionOnly } = options;
|
||||
if (!sheet) return undefined;
|
||||
|
||||
const facets = deriveDynamicFacets(sheet, dataset, activeProfile, reviews);
|
||||
const facets = selectWorkFacets(sheet, { dataset, activeProfile, reviews, statusFilter, facetFilters, pathFindingIds, search, actionOnly });
|
||||
const severityFacet = facets.find(facet => facet.severityScale && facet.kind === 'category');
|
||||
if (!severityFacet) return undefined;
|
||||
|
||||
const baseRecords = filterAndSortRecords(sheet, {
|
||||
activeProfile,
|
||||
search,
|
||||
sortColumn: 'all',
|
||||
sortDirection: 'desc',
|
||||
statusFilter: 'all',
|
||||
facetFilters: {},
|
||||
pathFindingIds,
|
||||
dataset,
|
||||
actionOnly,
|
||||
reviews
|
||||
});
|
||||
const countedSeverity = recountFacetsAgainstActiveFilters({
|
||||
facets,
|
||||
records: baseRecords,
|
||||
filters: facetFilters,
|
||||
reviews,
|
||||
statusFilter,
|
||||
matchesStatus: (record, status) => activeProfile.matchesStatus(record, status)
|
||||
}).find(facet => facet.id === severityFacet.id);
|
||||
|
||||
const counts = new Map(countedSeverity?.values.map(value => [value.value, value.count]) ?? []);
|
||||
const counts = new Map(severityFacet.values.map(value => [value.value, value.count]));
|
||||
const total = STANDARD_SEVERITIES.reduce((sum, severity) => sum + (counts.get(severity) ?? 0), 0);
|
||||
return {
|
||||
facetId: severityFacet.id,
|
||||
|
|
@ -105,7 +81,7 @@ export function SidebarSeverityMetrics({ sheet }: { sheet: DatasetSheet | undefi
|
|||
actionOnly,
|
||||
setFacetFilter
|
||||
} = useApp();
|
||||
const summary = useMemo(() => summarizeSeverities({
|
||||
const summary = useMemo(() => measureNavigationStep('severity-summary', () => summarizeSeverities({
|
||||
sheet,
|
||||
dataset,
|
||||
activeProfile,
|
||||
|
|
@ -115,7 +91,7 @@ export function SidebarSeverityMetrics({ sheet }: { sheet: DatasetSheet | undefi
|
|||
pathFindingIds,
|
||||
search,
|
||||
actionOnly
|
||||
}), [actionOnly, activeProfile, dataset, facetFilters, pathFindingIds, reviews, search, sheet, statusFilter]);
|
||||
})), [actionOnly, activeProfile, dataset, facetFilters, pathFindingIds, reviews, search, sheet, statusFilter]);
|
||||
if (!summary) return null;
|
||||
|
||||
const selected = facetFilters[summary.facetId]?.values ?? [];
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ export interface VirtualRowsResult {
|
|||
export function useVirtualRows(rowCount: number, rowHeight: number, overscan = 6): VirtualRowsResult {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
const containerRef = useCallback((node: HTMLElement | null) => setContainer(node), []);
|
||||
const [range, setRange] = useState({ start: 0, end: Math.min(rowCount, 40) });
|
||||
const [range, setRange] = useState(() => ({ start: 0,
|
||||
end: Math.min(rowCount, Math.ceil((typeof window === 'undefined' ? 720 : window.innerHeight) / rowHeight) + overscan * 2) }));
|
||||
const frame = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -76,6 +77,7 @@ export function useVirtualRows(rowCount: number, rowHeight: number, overscan = 6
|
|||
container.removeEventListener('scroll', onScroll);
|
||||
resizeObserver.disconnect();
|
||||
if (frame.current !== undefined) cancelAnimationFrame(frame.current);
|
||||
frame.current = undefined;
|
||||
};
|
||||
}, [container, rowCount, rowHeight, overscan]);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
CandidateMappingResult
|
||||
} from '../domain/candidate-mapping';
|
||||
import type { ImportProfile } from '../domain/import-profile';
|
||||
import type { ProfileFieldRule } from '../domain/tool-profile-definition';
|
||||
import type { ImportRecognitionResult, RecognitionOverrides } from '../domain/import-recognition';
|
||||
import type { FindingCandidate, SourceExtractionResult, SourceFieldCandidate } from '../domain/source-extraction';
|
||||
import { recognitionConfidence } from '../recognition/confidence';
|
||||
|
|
@ -10,34 +11,45 @@ import { evaluateFieldRoleEvidence } from '../recognition/field-role-evidence';
|
|||
|
||||
interface CandidateFieldGroup {
|
||||
id: string;
|
||||
identity: string;
|
||||
fieldKey: string;
|
||||
sourceLabel: string;
|
||||
tableIndex?: number;
|
||||
values: string[];
|
||||
headers: string[];
|
||||
}
|
||||
|
||||
function groupIdentity(field: SourceFieldCandidate): string {
|
||||
return JSON.stringify([field.source.tableIndex ?? null, field.key, field.label]);
|
||||
}
|
||||
|
||||
function fieldGroups(candidates: FindingCandidate[]): CandidateFieldGroup[] {
|
||||
function fieldGroups(candidates: FindingCandidate[], rules: ProfileFieldRule[]) {
|
||||
const groups = new Map<string, CandidateFieldGroup>();
|
||||
candidates.forEach(candidate => candidate.fields.forEach(field => {
|
||||
const identity = groupIdentity(field);
|
||||
const existing = groups.get(identity);
|
||||
if (existing) {
|
||||
if (existing.values.length < 250 && field.decodedValue.trim()) existing.values.push(field.decodedValue.trim());
|
||||
return;
|
||||
}
|
||||
groups.set(identity, {
|
||||
id: `candidate-field-${groups.size}`,
|
||||
fieldKey: field.key,
|
||||
sourceLabel: field.label,
|
||||
tableIndex: field.source.tableIndex,
|
||||
values: field.decodedValue.trim() ? [field.decodedValue.trim()] : []
|
||||
const contexts = new Map<FindingCandidate, string>();
|
||||
const conditions = rules.flatMap(rule => [...(rule.whenHeadersPresent ?? []), ...(rule.whenHeadersAbsent ?? [])]);
|
||||
candidates.forEach(candidate => {
|
||||
const headers = conditions.length ? candidate.fields.map(item => item.label) : [];
|
||||
const contextKey = conditions.map(pattern => headers.some(header => { pattern.lastIndex = 0; return pattern.test(header.trim()); }) ? '1' : '0').join('');
|
||||
contexts.set(candidate, contextKey);
|
||||
candidate.fields.forEach(field => {
|
||||
const identity = `${groupIdentity(field)}:${contextKey}`;
|
||||
const existing = groups.get(identity);
|
||||
if (existing) {
|
||||
if (existing.values.length < 250 && field.decodedValue.trim()) existing.values.push(field.decodedValue.trim());
|
||||
return;
|
||||
}
|
||||
groups.set(identity, {
|
||||
id: `candidate-field-${groups.size}`,
|
||||
identity,
|
||||
fieldKey: field.key,
|
||||
sourceLabel: field.label,
|
||||
tableIndex: field.source.tableIndex,
|
||||
headers,
|
||||
values: field.decodedValue.trim() ? [field.decodedValue.trim()] : []
|
||||
});
|
||||
});
|
||||
}));
|
||||
return [...groups.values()];
|
||||
});
|
||||
return { groups: [...groups.values()], contexts };
|
||||
}
|
||||
|
||||
function confirmedFieldDecision(
|
||||
|
|
@ -75,7 +87,7 @@ function mapGroup(
|
|||
requiresConfirmation: false
|
||||
};
|
||||
}
|
||||
const alternatives = evaluateFieldRoleEvidence(group.sourceLabel, group.values, profile.definition.fieldRules);
|
||||
const alternatives = evaluateFieldRoleEvidence(group.sourceLabel, group.values, profile.definition.fieldRules, group.headers);
|
||||
const best = alternatives[0];
|
||||
const proposedRole = best && best.confidence.level !== 'unknown' ? best.role : null;
|
||||
const automaticallyApplied = best && ['high', 'medium'].includes(best.confidence.level);
|
||||
|
|
@ -100,7 +112,7 @@ export function mapFindingCandidates(
|
|||
recognition?: ImportRecognitionResult,
|
||||
overrides?: RecognitionOverrides
|
||||
): CandidateMappingResult {
|
||||
const groups = fieldGroups(extraction.candidates);
|
||||
const { groups, contexts } = fieldGroups(extraction.candidates, profile.definition.fieldRules);
|
||||
const candidateRoleOverrides = new Map((overrides?.candidateFieldRoles ?? []).map(item => [item.decisionId, item.role]));
|
||||
const fieldDecisions = groups.map(group => mapGroup(group, profile, recognition)).map(decision => {
|
||||
const role = decision.sourceScope.tableIndex === undefined
|
||||
|
|
@ -119,7 +131,7 @@ export function mapFindingCandidates(
|
|||
};
|
||||
});
|
||||
const decisionsByIdentity = new Map(groups.map((group, index) => [
|
||||
JSON.stringify([group.tableIndex ?? null, group.fieldKey, group.sourceLabel]),
|
||||
group.identity,
|
||||
fieldDecisions[index]
|
||||
]));
|
||||
const mappingProfile = {
|
||||
|
|
@ -137,7 +149,7 @@ export function mapFindingCandidates(
|
|||
id: candidate.id,
|
||||
source: candidate.source,
|
||||
fields: candidate.fields.map(field => {
|
||||
const decision = decisionsByIdentity.get(groupIdentity(field));
|
||||
const decision = decisionsByIdentity.get(`${groupIdentity(field)}:${contexts.get(candidate) ?? ''}`);
|
||||
if (!decision) throw new Error(`Keine Mappingentscheidung für Quellfeld ${field.key} vorhanden.`);
|
||||
return {
|
||||
...field,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { sourceNativeFindingIdentity, unresolvedFindingIdentity } from '../domai
|
|||
import { projectMappedCandidates } from './canonical-candidate-projection';
|
||||
import { cisCatResultCategory, compareCisCatSummary, describeCisCatSummaryMismatch, extractCisCatSummary } from './cis-cat-summary';
|
||||
import { normalizeStatusDimensionsFor } from './status-normalization';
|
||||
import { defaultSecurityScoringPolicy } from '../domain/security-scoring';
|
||||
import { processingManifest } from '../domain/processing-compatibility';
|
||||
|
||||
export interface CanonicalImportBatch {
|
||||
|
|
@ -468,7 +469,7 @@ export function createCanonicalImportBatch(
|
|||
const model: CanonicalAssessmentModel = {
|
||||
schemaVersion: 1,
|
||||
project: { id: projectId, name: projectName, createdAt: importedAt, updatedAt: importedAt, attributes: { importBatchId: batchId } },
|
||||
assessment: { id: assessmentId, projectId, name: `Assessment ${importedAt.slice(0, 10)}`, createdAt: importedAt, updatedAt: importedAt },
|
||||
assessment: { id: assessmentId, projectId, name: `Assessment ${importedAt.slice(0, 10)}`, createdAt: importedAt, updatedAt: importedAt, scoringPolicy: defaultSecurityScoringPolicy() },
|
||||
scanRuns: [{ id: scanRunId, assessmentId, sequence: 1, label: 'Import 1', tool: runTool, importedAt }],
|
||||
importFiles,
|
||||
sourceTables,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
/**
|
||||
* Umgebungskontext eines CIS-CAT-Berichts.
|
||||
*
|
||||
* Der Bericht traegt neben den Regelergebnissen drei Angaben, die kein
|
||||
* einzelnes Ergebnis kennt: welches System bewertet wurde, gegen welchen
|
||||
* Benchmark, und wie der Bericht sein Gesamtergebnis selbst beziffert
|
||||
* (Gesamtscore, Ergebniszahlen je Zustand, Punktwert je Benchmarkabschnitt).
|
||||
* Bisher blieben diese Angaben ausschliesslich im Dateinamen und in
|
||||
* Quelltabellen liegen.
|
||||
*
|
||||
* Bewusste Grenzen dieser Auswertung:
|
||||
*
|
||||
* - Es wird nur gelesen, was der Bericht selbst beschriftet. Systemangaben
|
||||
* entstehen aus beschrifteten Wertepaaren ("Hostname: ..."), nicht aus einer
|
||||
* angenommenen Stelle im Aufbau. Fehlt die Beschriftung, entsteht keine
|
||||
* Beobachtung - die Ansicht zeigt dann einen Gedankenstrich statt eines
|
||||
* geratenen Wertes.
|
||||
* - Der Abschnittsscore stammt aus der Uebersichtstabelle des Berichts. Nur
|
||||
* deren oberste Gliederungsebene ("1 Account Policies") wird als Beobachtung
|
||||
* gefuehrt; die vollstaendige Tabelle bleibt als Berichtsabschnitt erhalten.
|
||||
* - Native CIS-CAT-Zahlen bleiben unter `benchmark.*` und werden nie in eine
|
||||
* werkzeuguebergreifende Kennzahl gemischt.
|
||||
*/
|
||||
|
||||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { EnvironmentSubject } from '../domain/environment-context';
|
||||
import type { DecodedSourceDocument, ImportTable } from '../domain/import-intermediate';
|
||||
import type { EnvironmentObservationCandidate, ScannerReportSectionCandidate } from '../domain/source-extraction';
|
||||
import { clean, normalizeNumericValue } from '../shared/text';
|
||||
import { markupText } from './html-extraction';
|
||||
import { CIS_CAT_RESULT_CATEGORIES, extractCisCatSummary } from './cis-cat-summary';
|
||||
|
||||
/** Titelzeile des Berichts: "Benchmark Result - <Benchmark>". */
|
||||
const REPORT_TITLE = /<title\b[^>]*>([\s\S]{0,400}?)<\/title\s*>/i;
|
||||
const BENCHMARK_TITLE_PREFIX = /^benchmark\s+result\s*[-–—:]?\s*/i;
|
||||
/**
|
||||
* Ersatzweise steht in der Titelzeile die XCCDF-Ergebniskennung statt eines
|
||||
* lesbaren Namens: "xccdf_org.cisecurity.benchmarks_testresult_2.0.0_CIS_...".
|
||||
*/
|
||||
const XCCDF_TESTRESULT = /^xccdf[_.][\w.]*testresult[_.](\d+(?:\.\d+)*)[_.](.+)$/i;
|
||||
/** Titelseite: Berichtskopf mit bewertetem System und Zielnetzadresse. */
|
||||
const COVER_TITLE_BLOCK = /<div\b[^>]*\bid\s*=\s*["']coverPageTitle["'][^>]*>([\s\S]{0,4000}?)<\/div\s*>/i;
|
||||
/** Titelseite: Benchmarkzeile, Profil, Erhebungszeitpunkt und Dauer. */
|
||||
const COVER_SUBTITLE_BLOCK = /<div\b[^>]*\bid\s*=\s*["']coverPageSubTitle["'][^>]*>([\s\S]{0,4000}?)<\/div\s*>/i;
|
||||
const COVER_HOST = /^for\s+(.+)$/i;
|
||||
const COVER_IP = /^(?:target\s+)?ip\s*address\s*:\s*(.+)$/i;
|
||||
const COVER_BENCHMARK = /^(.*?)\s+v(\d+(?:\.\d+)*)$/;
|
||||
const COVER_DURATION = /^assessment\s+duration\s*:\s*(.+)$/i;
|
||||
const ASSESSOR_ENGINE = /CIS-CAT\s+Pro\s+Assessor\)?\s*v(\d+(?:\.\d+)*)/i;
|
||||
const LIST_ITEM = /<li\b[^>]*>([\s\S]{0,400}?)<\/li\s*>/gi;
|
||||
const HEADING = /<h([12])\b[^>]*>([\s\S]{0,400}?)<\/h\1\s*>/gi;
|
||||
/** Punktwert der Bewertungsanzeige im Summary-Block. */
|
||||
const GAUGE_SCORE = /data-score\s*=\s*["']?(-?\d+(?:[.,]\d+)?)["']?/i;
|
||||
/** Oberste Gliederungsebene der Abschnittsuebersicht ("1 Account Policies"). */
|
||||
const TOP_LEVEL_SECTION = /^(\d+)\s+(.+)$/;
|
||||
const MAXIMUM_LABEL_SCAN_CHARACTERS = 200_000;
|
||||
|
||||
/**
|
||||
* Beschriftete Systemangaben. Der Bericht fuehrt sie als Wertepaar - entweder
|
||||
* in einer zweispaltigen Tabelle oder als "Beschriftung: Wert" im Kopfbereich.
|
||||
*/
|
||||
const SYSTEM_FACTS: Array<[RegExp, string, string]> = [
|
||||
[/^host\s*name$|^hostname$|^system\s*name$/i, 'host.name', 'Assessed host'],
|
||||
[/^operating\s*system$|^os\s*name$|^os$/i, 'operating_system.name', 'Operating system'],
|
||||
[/^ip\s*address(?:es)?$/i, 'host.ip_address', 'IP address'],
|
||||
[/^mac\s*address(?:es)?$/i, 'host.mac_address', 'MAC address'],
|
||||
[/^domain$|^domain\s*name$/i, 'host.domain', 'Host domain'],
|
||||
[/^assessment\s*(?:date|time|timestamp)$|^scan\s*(?:date|time)$/i, 'report.generated_at', 'Report generation date'],
|
||||
[/^benchmark\s*version$/i, 'benchmark.version', 'Benchmark version'],
|
||||
[/^profile$|^selected\s*profile$/i, 'benchmark.profile', 'Benchmark profile']
|
||||
];
|
||||
|
||||
function propertyKey(value: string): string {
|
||||
return value.toLocaleLowerCase('en-US').replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'value';
|
||||
}
|
||||
|
||||
function assessedSystem(document: DecodedSourceDocument): EnvironmentSubject {
|
||||
return { type: 'assessed-system', identity: document.file.name, label: 'CIS-CAT assessed system' };
|
||||
}
|
||||
|
||||
function benchmarkSubject(document: DecodedSourceDocument): EnvironmentSubject {
|
||||
return { type: 'benchmark-assessment', identity: document.file.name, label: 'CIS-CAT benchmark assessment' };
|
||||
}
|
||||
|
||||
function observation(
|
||||
document: DecodedSourceDocument,
|
||||
subject: EnvironmentSubject,
|
||||
property: string,
|
||||
label: string,
|
||||
value: string | number,
|
||||
objectPath: string
|
||||
): EnvironmentObservationCandidate {
|
||||
const numeric = typeof value === 'number' ? value : normalizeNumericValue(value);
|
||||
return {
|
||||
version: 1,
|
||||
subject,
|
||||
property,
|
||||
label,
|
||||
originalValue: value as JsonValue,
|
||||
normalizedValue: (numeric !== null ? numeric : value) as JsonValue,
|
||||
valueType: numeric !== null ? 'number' : 'string',
|
||||
state: 'observed',
|
||||
sectionKey: 'cis-cat-assessment-summary',
|
||||
source: { fileName: document.file.name, objectPath },
|
||||
attributes: {}
|
||||
};
|
||||
}
|
||||
|
||||
function benchmarkName(document: DecodedSourceDocument): string {
|
||||
const title = clean(markupText(REPORT_TITLE.exec(document.rawText)?.[1] ?? '')).replace(BENCHMARK_TITLE_PREFIX, '');
|
||||
const identifier = XCCDF_TESTRESULT.exec(title);
|
||||
return identifier ? clean(identifier[2].replace(/[_.]+/g, ' ')) : title;
|
||||
}
|
||||
|
||||
function blockItems(block: string, pattern: RegExp): string[] {
|
||||
pattern.lastIndex = 0;
|
||||
const items: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(block))) items.push(clean(markupText(match[match.length - 1] ?? '')));
|
||||
return items.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Angaben der Berichtstitelseite.
|
||||
*
|
||||
* CIS-CAT Pro fuehrt bewertetes System, Zielnetzadresse, Benchmark samt
|
||||
* Version, gewaehltes Profil, Erhebungszeitpunkt und Dauer ausschliesslich hier
|
||||
* - nicht als beschriftete Tabelle. Fehlt der Block, entsteht keine Beobachtung.
|
||||
*/
|
||||
function coverPageObservations(document: DecodedSourceDocument): EnvironmentObservationCandidate[] {
|
||||
const observations: EnvironmentObservationCandidate[] = [];
|
||||
const add = (subject: EnvironmentSubject, property: string, label: string, value: string, objectPath: string) => {
|
||||
if (value) observations.push(observation(document, subject, property, label, value, objectPath));
|
||||
};
|
||||
const title = COVER_TITLE_BLOCK.exec(document.rawText)?.[1];
|
||||
if (title) {
|
||||
const host = blockItems(title, HEADING).flatMap(heading => COVER_HOST.exec(heading)?.[1] ?? []);
|
||||
add(assessedSystem(document), 'host.name', 'Assessed host', clean(host[0] ?? ''), 'html/cover/host');
|
||||
const address = blockItems(title, LIST_ITEM).flatMap(item => COVER_IP.exec(item)?.[1] ?? []);
|
||||
add(assessedSystem(document), 'host.ip_address', 'IP address', clean(address[0] ?? ''), 'html/cover/ip-address');
|
||||
}
|
||||
const subtitle = COVER_SUBTITLE_BLOCK.exec(document.rawText)?.[1];
|
||||
if (subtitle) {
|
||||
const heading = blockItems(subtitle, HEADING)[0] ?? '';
|
||||
const benchmark = COVER_BENCHMARK.exec(heading);
|
||||
add(benchmarkSubject(document), 'benchmark.name', 'Benchmark', clean(benchmark?.[1] ?? heading), 'html/cover/benchmark');
|
||||
add(benchmarkSubject(document), 'benchmark.version', 'Benchmark version', clean(benchmark?.[2] ?? ''), 'html/cover/benchmark-version');
|
||||
const items = blockItems(subtitle, LIST_ITEM);
|
||||
const duration = items.flatMap(item => COVER_DURATION.exec(item)?.[1] ?? []);
|
||||
const remaining = items.filter(item => !COVER_DURATION.test(item));
|
||||
add(benchmarkSubject(document), 'benchmark.profile', 'Benchmark profile', remaining[0] ?? '', 'html/cover/profile');
|
||||
add(benchmarkSubject(document), 'report.generated_at', 'Report generation date', remaining[1] ?? '', 'html/cover/generated-at');
|
||||
add(benchmarkSubject(document), 'benchmark.assessment_duration', 'Assessment duration', clean(duration[0] ?? ''), 'html/cover/duration');
|
||||
}
|
||||
const engine = ASSESSOR_ENGINE.exec(document.rawText)?.[1];
|
||||
add(benchmarkSubject(document), 'report.engine_version', 'CIS-CAT engine version', clean(engine ?? ''), 'html/cover/engine');
|
||||
return observations;
|
||||
}
|
||||
|
||||
/** Wertepaare aus zweispaltigen Tabellen und beschrifteten Kopfangaben. */
|
||||
function labelledFacts(document: DecodedSourceDocument): Array<{ label: string; value: string; objectPath: string }> {
|
||||
const pairs: Array<{ label: string; value: string; objectPath: string }> = [];
|
||||
document.tables.forEach(table => table.rows.forEach(row => {
|
||||
// Die Kopfzeile beschriftet die Spalten, nicht den Wert daneben. Ohne
|
||||
// diese Ausnahme laese sich aus "Profile | Selected" die Angabe
|
||||
// "Profile: Selected" ableiten.
|
||||
if (row.cells.length !== 2 || row.index === table.headerCandidates[0]?.rowIndex) return;
|
||||
const label = clean(row.cells[0]?.decodedValue).replace(/:$/, '');
|
||||
const value = clean(row.cells[1]?.decodedValue);
|
||||
if (label && value) pairs.push({ label, value, objectPath: `html/tables/${table.index}/rows/${row.index}` });
|
||||
}));
|
||||
const head = document.rawText.slice(0, MAXIMUM_LABEL_SCAN_CHARACTERS);
|
||||
const labelled = /(?:^|>|\n)\s*([A-Za-z][A-Za-z ]{2,28})\s*:\s*([^<\n]{1,160})/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = labelled.exec(head))) {
|
||||
const label = clean(match[1]);
|
||||
const value = clean(match[2]);
|
||||
if (label && value) pairs.push({ label, value, objectPath: `html/facts/${propertyKey(label)}` });
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
function systemObservations(document: DecodedSourceDocument): EnvironmentObservationCandidate[] {
|
||||
const seen = new Set<string>();
|
||||
return labelledFacts(document).flatMap(pair => {
|
||||
const fact = SYSTEM_FACTS.find(([pattern]) => pattern.test(pair.label));
|
||||
if (!fact || seen.has(fact[1])) return [];
|
||||
seen.add(fact[1]);
|
||||
const subject = fact[1].startsWith('benchmark.') || fact[1] === 'report.generated_at'
|
||||
? benchmarkSubject(document)
|
||||
: assessedSystem(document);
|
||||
return [observation(document, subject, fact[1], fact[2], pair.value, pair.objectPath)];
|
||||
});
|
||||
}
|
||||
|
||||
function summaryObservations(document: DecodedSourceDocument): EnvironmentObservationCandidate[] {
|
||||
const observations: EnvironmentObservationCandidate[] = [];
|
||||
const name = benchmarkName(document);
|
||||
if (name) {
|
||||
observations.push(observation(document, benchmarkSubject(document), 'benchmark.name', 'Benchmark', name, 'html/title'));
|
||||
}
|
||||
const score = GAUGE_SCORE.exec(document.rawText)?.[1];
|
||||
const numericScore = score === undefined ? null : normalizeNumericValue(score);
|
||||
if (numericScore !== null) {
|
||||
observations.push(observation(
|
||||
document, benchmarkSubject(document), 'benchmark.score', 'Benchmark score', numericScore, 'html/summary/gauge'
|
||||
));
|
||||
}
|
||||
const summary = extractCisCatSummary(document.rawText);
|
||||
if (summary) {
|
||||
CIS_CAT_RESULT_CATEGORIES.forEach(category => observations.push(observation(
|
||||
document, benchmarkSubject(document), `benchmark.result_count.${category}`,
|
||||
`Recommendations ${category}`, summary.counts[category], `html/summary/${category}`
|
||||
)));
|
||||
if (summary.scoredTotal !== undefined) observations.push(observation(
|
||||
document, benchmarkSubject(document), 'benchmark.scored_total', 'Scored recommendations',
|
||||
summary.scoredTotal, 'html/summary/scored-total'
|
||||
));
|
||||
if (summary.scoredPassed !== undefined) observations.push(observation(
|
||||
document, benchmarkSubject(document), 'benchmark.scored_passed', 'Scored recommendations passed',
|
||||
summary.scoredPassed, 'html/summary/scored-passed'
|
||||
));
|
||||
}
|
||||
return observations;
|
||||
}
|
||||
|
||||
/** Uebersichtstabelle mit Punktwert je Benchmarkabschnitt. */
|
||||
function sectionScoreTable(document: DecodedSourceDocument): ImportTable | undefined {
|
||||
return document.tables.find(table => {
|
||||
const headers = table.headerCandidates[0]?.headers.map(value => clean(value)) ?? [];
|
||||
const has = (pattern: RegExp) => headers.some(header => pattern.test(header));
|
||||
return has(/^description$/i) && has(/^pass$/i) && has(/^fail$/i) && has(/^percent$/i);
|
||||
});
|
||||
}
|
||||
|
||||
function sectionScoreContext(document: DecodedSourceDocument): {
|
||||
observations: EnvironmentObservationCandidate[];
|
||||
section?: ScannerReportSectionCandidate;
|
||||
} {
|
||||
const table = sectionScoreTable(document);
|
||||
const header = table?.headerCandidates[0];
|
||||
if (!table || !header) return { observations: [] };
|
||||
const headers = header.headers.map(value => clean(value));
|
||||
const column = (pattern: RegExp) => headers.findIndex(value => pattern.test(value));
|
||||
const descriptionIndex = column(/^description$/i);
|
||||
const percentIndex = column(/^percent$/i);
|
||||
const rows = table.rows.filter(row => row.index !== header.rowIndex);
|
||||
const observations = rows.flatMap(row => {
|
||||
const description = clean(row.cells[descriptionIndex]?.decodedValue);
|
||||
const section = TOP_LEVEL_SECTION.exec(description);
|
||||
const percent = normalizeNumericValue(row.cells[percentIndex]?.decodedValue);
|
||||
if (!section || percent === null) return [];
|
||||
return [observation(
|
||||
document, { type: 'benchmark-section', identity: section[1], label: clean(section[2]) },
|
||||
`benchmark.section_score.${section[1]}`, clean(section[2]), percent,
|
||||
`html/tables/${table.index}/rows/${row.index}`
|
||||
)];
|
||||
});
|
||||
return {
|
||||
observations,
|
||||
section: {
|
||||
version: 1,
|
||||
sectionKey: 'cis-cat-section-scores',
|
||||
label: 'Benchmark section scores',
|
||||
ordinal: 1,
|
||||
state: rows.length ? 'observed' : 'empty',
|
||||
tables: [{
|
||||
name: 'Section scores',
|
||||
sourceTableIndex: table.index,
|
||||
headers,
|
||||
rows: rows.map(row => headers.map((_, index) => clean(row.cells[index]?.decodedValue)))
|
||||
}],
|
||||
source: { fileName: document.file.name, tableIndex: table.index },
|
||||
attributes: { rowCount: rows.length }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function cisCatEnvironmentContext(document: DecodedSourceDocument): {
|
||||
environmentObservations: EnvironmentObservationCandidate[];
|
||||
reportSections: ScannerReportSectionCandidate[];
|
||||
} {
|
||||
const sectionScores = sectionScoreContext(document);
|
||||
// Reihenfolge ist die Rangfolge: die Titelseite nennt System und Benchmark
|
||||
// ausdruecklich, beschriftete Wertepaare sind nur der Rueckfall.
|
||||
const seen = new Set<string>();
|
||||
const environmentObservations = [
|
||||
...coverPageObservations(document),
|
||||
...systemObservations(document),
|
||||
...summaryObservations(document),
|
||||
...sectionScores.observations
|
||||
].filter(item => !seen.has(item.property) && seen.add(item.property));
|
||||
const summarySection: ScannerReportSectionCandidate = {
|
||||
version: 1,
|
||||
sectionKey: 'cis-cat-assessment-summary',
|
||||
label: 'CIS-CAT assessment summary',
|
||||
ordinal: 0,
|
||||
state: environmentObservations.length ? 'observed' : 'empty',
|
||||
tables: [],
|
||||
source: { fileName: document.file.name, objectPath: 'html/summary' },
|
||||
attributes: { observationCount: environmentObservations.length }
|
||||
};
|
||||
return {
|
||||
environmentObservations,
|
||||
reportSections: [summarySection, ...(sectionScores.section ? [sectionScores.section] : [])]
|
||||
};
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@ import type { DecodedSourceDocument, ImportTable, SourceLocator } from '../domai
|
|||
import type { FindingCandidate, SourceExtractor, SourceFieldCandidate } from '../domain/source-extraction';
|
||||
import { clean } from '../shared/text';
|
||||
import { cleanLineBreaks } from '../shared/markdown';
|
||||
import { cisCatEnvironmentContext } from './cis-cat-environment-context';
|
||||
import { cisCatResultCategory } from './cis-cat-summary';
|
||||
import { decodeHtmlEntities, findHtmlTableSpans, isSafeLinkTarget, markupText, sanitizeHtmlText } from './html-extraction';
|
||||
import { htmlAttributeValue } from './html-attributes';
|
||||
|
||||
interface HtmlDivBlock {
|
||||
start: number;
|
||||
|
|
@ -64,14 +66,7 @@ const BOLD_CLASS = /(?:^|\s)bold(?:$|\s)/i;
|
|||
const CAPTION_CLASS = /(?:^|\s)caption(?:$|\s)/i;
|
||||
|
||||
function attributeValue(attributes: string, name: string): string {
|
||||
const pattern = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(attributes))) {
|
||||
if (match[1]?.toLocaleLowerCase('en-US') === name.toLocaleLowerCase('en-US')) {
|
||||
return markupText(match[2] ?? match[3] ?? match[4] ?? '');
|
||||
}
|
||||
}
|
||||
return '';
|
||||
return markupText(htmlAttributeValue(attributes, name));
|
||||
}
|
||||
|
||||
function scanDivBlocks(html: string): HtmlDivBlock[] {
|
||||
|
|
@ -551,7 +546,7 @@ function candidateFields(
|
|||
|
||||
export const cisCatSourceExtractor: SourceExtractor = {
|
||||
id: 'cis-cat-html-source-extractor',
|
||||
version: '1.2.0',
|
||||
version: '1.3.1',
|
||||
detect(document) {
|
||||
if (document.format !== 'HTML') return { score: 0, evidence: [] };
|
||||
const resultTable = document.tables.find(table =>
|
||||
|
|
@ -569,15 +564,19 @@ export const cisCatSourceExtractor: SourceExtractor = {
|
|||
};
|
||||
},
|
||||
extract(document) {
|
||||
// Der Umgebungskontext haengt nicht an der Ergebnistabelle: auch ein
|
||||
// Bericht ohne auswertbare Regelzeilen nennt System und Benchmark.
|
||||
const context = cisCatEnvironmentContext(document);
|
||||
const resultTable = document.tables.find(table =>
|
||||
table.attributes.elementId === 'assessmentResultTable' || RESULT_TABLE_NAME.test(clean(table.name))
|
||||
);
|
||||
if (!resultTable) return { version: 1, candidates: [], diagnostics: [] };
|
||||
if (!resultTable) return { version: 1, candidates: [], ...context, diagnostics: [] };
|
||||
const tableHeader = headers(resultTable);
|
||||
if (!tableHeader) {
|
||||
return {
|
||||
version: 1,
|
||||
candidates: [],
|
||||
...context,
|
||||
diagnostics: [{
|
||||
severity: 'warning',
|
||||
code: 'cis-cat-result-header-missing',
|
||||
|
|
@ -591,6 +590,7 @@ export const cisCatSourceExtractor: SourceExtractor = {
|
|||
return {
|
||||
version: 1,
|
||||
candidates: [],
|
||||
...context,
|
||||
diagnostics: [{
|
||||
severity: 'warning',
|
||||
code: 'cis-cat-result-columns-missing',
|
||||
|
|
@ -639,6 +639,6 @@ export const cisCatSourceExtractor: SourceExtractor = {
|
|||
details: { missingDetails }
|
||||
}]
|
||||
: [];
|
||||
return { version: 1, candidates, diagnostics };
|
||||
return { version: 1, candidates, ...context, diagnostics };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export async function importSource(
|
|||
message: 'Der Parser und die registrierten Quellenextraktoren haben keine auswertbaren Inhalte geliefert.'
|
||||
}]);
|
||||
}
|
||||
const recognition = imported.intermediate ? recognizeImportDocument(imported.intermediate) : undefined;
|
||||
const recognition = imported.intermediate ? recognizeImportDocument(imported.intermediate, imported.extraction) : undefined;
|
||||
const profile = recognition
|
||||
? selectImportProfile(recognition, options.profiles)
|
||||
: options.profiles.find(candidate => candidate.definition.fallback) ?? options.profiles[0];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import type { SourceExtractor } from '../domain/source-extraction';
|
||||
|
||||
function safeId(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||
}
|
||||
|
||||
export const genericTableSourceExtractor: SourceExtractor = {
|
||||
id: 'generic-table-source-extractor',
|
||||
version: '1.0.0',
|
||||
fallback: true,
|
||||
detect(document) {
|
||||
const count = document.tables.length;
|
||||
return count
|
||||
? { score: 0.1, evidence: [`${count} kompatible Tabellenansicht(en) vorhanden.`] }
|
||||
: { score: 0, evidence: [] };
|
||||
},
|
||||
extract(document) {
|
||||
const candidates = document.tables.flatMap(table => {
|
||||
const header = table.headerCandidates[0];
|
||||
return table.rows
|
||||
.filter(row => row.index !== header?.rowIndex && row.cells.some(cell => cell.decodedValue.trim()))
|
||||
.map(row => ({
|
||||
version: 1 as const,
|
||||
id: `candidate-${safeId(table.id)}-${row.index}`,
|
||||
source: { fileName: document.file.name, tableIndex: table.index, rowIndex: row.index },
|
||||
fields: row.cells.map(cell => ({
|
||||
key: `column-${cell.source.columnIndex ?? 0}`,
|
||||
label: header?.headers[cell.source.columnIndex ?? 0] || `Spalte ${(cell.source.columnIndex ?? 0) + 1}`,
|
||||
originalValue: cell.rawValue,
|
||||
decodedValue: cell.decodedValue,
|
||||
source: { ...cell.source, fileName: document.file.name }
|
||||
}))
|
||||
}));
|
||||
});
|
||||
return { version: 1, candidates, diagnostics: [] };
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { ImportError } from '../shared/errors';
|
||||
|
||||
/**
|
||||
* Keep angle brackets inside quoted attributes inert for the bounded lexical
|
||||
* readers. This is not a sanitizer: values and entities remain unchanged apart
|
||||
* from those delimiters. Raw-text elements and comments are never interpreted
|
||||
* as nested tags. The original document remains untouched.
|
||||
*/
|
||||
export function protectHtmlAttributeMarkup(text: string, allowIncompleteFinalTag = false): string {
|
||||
const output: string[] = [];
|
||||
let copied = 0;
|
||||
let position = 0;
|
||||
while ((position = text.indexOf('<', position)) >= 0) {
|
||||
if (text.startsWith('<!--', position)) {
|
||||
const end = text.indexOf('-->', position + 4);
|
||||
position = end < 0 ? text.length : end + 3;
|
||||
continue;
|
||||
}
|
||||
const opening = /^<\/?([a-z][\w:-]*)(?=[\s/>])/i.exec(text.slice(position, position + 256));
|
||||
if (!opening) { position++; continue; }
|
||||
const start = position;
|
||||
const previousOutputLength = output.length;
|
||||
const previousCopied = copied;
|
||||
let quote = '';
|
||||
let expectingValue = false;
|
||||
let unquotedValue = false;
|
||||
let closed = false;
|
||||
for (position += opening[0].length; position < text.length; position++) {
|
||||
const character = text[position];
|
||||
if (quote) {
|
||||
if (character === quote) quote = '';
|
||||
else if (character === '<' || character === '>') {
|
||||
output.push(text.slice(copied, position), character === '<' ? '<' : '>');
|
||||
copied = position + 1;
|
||||
}
|
||||
} else if (character === '>') { position++; closed = true; break; }
|
||||
else if (unquotedValue) {
|
||||
if (/\s/.test(character)) unquotedValue = false;
|
||||
} else if (expectingValue) {
|
||||
if (/\s/.test(character)) continue;
|
||||
expectingValue = false;
|
||||
if (character === '"' || character === "'") quote = character;
|
||||
else unquotedValue = true;
|
||||
} else if (character === '=') expectingValue = true;
|
||||
}
|
||||
// Bounded text fragments may end within a tag even though the complete
|
||||
// document was valid. Drop only that unfinished tag, never expose its tail.
|
||||
if (!closed && allowIncompleteFinalTag) return output.slice(0, previousOutputLength).join('') + text.slice(previousCopied, start);
|
||||
if (!closed && quote) throw new ImportError('HTML enthält ein nicht abgeschlossenes Attribut.', [{
|
||||
severity: 'error', code: 'html-unclosed-attribute', stage: 'structure-decoding',
|
||||
message: 'Eine Attributzeichenkette endet nicht innerhalb des HTML-Dokuments.'
|
||||
}]);
|
||||
if (text[start + 1] !== '/' && /^(script|style|textarea|title|xmp|iframe|noembed|noframes)$/i.test(opening[1])) {
|
||||
const closing = new RegExp('</' + opening[1] + '\\s*>', 'ig');
|
||||
closing.lastIndex = position;
|
||||
const end = closing.exec(text);
|
||||
position = end ? end.index + end[0].length : text.length;
|
||||
}
|
||||
}
|
||||
return output.length ? output.join('') + text.slice(copied) : text;
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** Attribute offsets refer to the supplied fragment, not a DOM. Values remain
|
||||
* raw; entity decoding and source-specific meaning belong to callers. */
|
||||
export interface HtmlAttribute {
|
||||
name: string;
|
||||
value: string | undefined;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** Read characters only a constant number of times, including boolean and
|
||||
* malformed attributes. An unanchored name=value regex retries every suffix
|
||||
* of a long valueless attribute and causes quadratic backtracking. */
|
||||
export function* htmlAttributes(text: string): Generator<HtmlAttribute> {
|
||||
let position = 0;
|
||||
while (position < text.length) {
|
||||
if (/[\s/=>]/.test(text[position])) { position++; continue; }
|
||||
const start = position;
|
||||
while (position < text.length && !/[\s/=>]/.test(text[position])) position++;
|
||||
const name = text.slice(start, position).toLowerCase();
|
||||
const nameEnd = position;
|
||||
while (position < text.length && /\s/.test(text[position])) position++;
|
||||
let value: string | undefined;
|
||||
if (text[position] === '=') {
|
||||
position++;
|
||||
while (position < text.length && /\s/.test(text[position])) position++;
|
||||
const quote = text[position];
|
||||
if (quote === '"' || quote === "'") {
|
||||
const valueStart = ++position;
|
||||
while (position < text.length && text[position] !== quote) position++;
|
||||
value = text.slice(valueStart, position);
|
||||
if (position < text.length) position++;
|
||||
} else {
|
||||
const valueStart = position;
|
||||
while (position < text.length && !/[\s>]/.test(text[position])) position++;
|
||||
value = text.slice(valueStart, position);
|
||||
}
|
||||
}
|
||||
yield { name, value, start, end: value === undefined ? nameEnd : position };
|
||||
}
|
||||
}
|
||||
|
||||
export function htmlAttributeValue(text: string, name: string): string {
|
||||
const normalizedName = name.toLowerCase();
|
||||
for (const attribute of htmlAttributes(text)) {
|
||||
if (attribute.name === normalizedName && attribute.value !== undefined) return attribute.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { htmlAttributes } from './html-attributes';
|
||||
|
||||
export interface IndexedHtmlElement {
|
||||
tagName: string;
|
||||
start: number;
|
||||
bodyStart: number;
|
||||
selfClosing: boolean;
|
||||
}
|
||||
|
||||
/** Import-local index over already protected/sanitized lexical markup. No DOM
|
||||
* or persistent cache: offsets belong only to this exact immutable string. */
|
||||
export function indexHtmlElements(markup: string) {
|
||||
const byId = new Map<string, IndexedHtmlElement>();
|
||||
const namedAnchors = new Map<string, IndexedHtmlElement>();
|
||||
const definitions = new Map<string, { count: number; start: number }>();
|
||||
const pattern = /<([a-zA-Z][\w:-]*)\b([^>]*)>/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(markup))) {
|
||||
let id: string | undefined;
|
||||
let name: string | undefined;
|
||||
for (const attribute of htmlAttributes(match[2] ?? '')) {
|
||||
if (attribute.name === 'id' && id === undefined) id = attribute.value;
|
||||
if (attribute.name === 'name' && name === undefined) name = attribute.value;
|
||||
}
|
||||
if (!id && !name) continue;
|
||||
const element = { tagName: match[1], start: match.index, bodyStart: pattern.lastIndex, selfClosing: /\/\s*>$/.test(match[0]) };
|
||||
if (id && !byId.has(id)) byId.set(id, element);
|
||||
if (name && element.tagName.toLowerCase() === 'a' && !namedAnchors.has(name)) namedAnchors.set(name, element);
|
||||
for (const target of new Set([id, name])) {
|
||||
if (!target) continue;
|
||||
const previous = definitions.get(target);
|
||||
if (previous) previous.count++;
|
||||
else definitions.set(target, { count: 1, start: element.start });
|
||||
}
|
||||
}
|
||||
return { byId, namedAnchors, definitions };
|
||||
}
|
||||
|
||||
export type HtmlElementIndex = ReturnType<typeof indexHtmlElements>;
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
import type { DecodedHtmlDocument, DecodedHtmlTable } from './runtime-adapters';
|
||||
import { protectHtmlAttributeMarkup } from './html-attribute-markup';
|
||||
import { htmlAttributes, htmlAttributeValue } from './html-attributes';
|
||||
|
||||
// Real-world scanner reports can exceed the isolated worker's time budget well
|
||||
// before four MiB when their markup expands into a large DOM. Route reports
|
||||
|
|
@ -270,7 +272,7 @@ export function decodeHtmlDocument(documentNode: Document): DecodedHtmlDocument
|
|||
|
||||
export function sanitizeHtmlText(text: string): { text: string; removedContentCount: number } {
|
||||
let removedContentCount = 0;
|
||||
let sanitized = text.replace(/<(script|style|iframe|object|embed|svg|form|template|canvas|audio|video)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, () => {
|
||||
let sanitized = protectHtmlAttributeMarkup(text).replace(/<(script|style|iframe|object|embed|svg|form|template|canvas|audio|video)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, () => {
|
||||
removedContentCount += 1;
|
||||
return ' ';
|
||||
});
|
||||
|
|
@ -278,32 +280,35 @@ export function sanitizeHtmlText(text: string): { text: string; removedContentCo
|
|||
removedContentCount += 1;
|
||||
return ' ';
|
||||
});
|
||||
sanitized = sanitized.replace(/\s(?:on[\w:-]*|srcdoc|src|action|formaction|xlink:href)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, () => {
|
||||
removedContentCount += 1;
|
||||
return '';
|
||||
});
|
||||
// Verweisziele bleiben als Web-Adresse erhalten, jedes andere Schema faellt
|
||||
// weiterhin heraus. Der Wert wird normalisiert neu geschrieben, damit keine
|
||||
// Anfuehrungszeichen aus der Quelle das Attribut verlassen koennen.
|
||||
sanitized = sanitized.replace(/\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (match, quoted, single, bare) => {
|
||||
const target = decodeHtmlEntities(quoted ?? single ?? bare ?? '').trim();
|
||||
if (isSafeLinkTarget(target)) return ` href="${escapeHtmlText(target)}"`;
|
||||
removedContentCount += 1;
|
||||
return '';
|
||||
// Read whole attributes within a tag. A tooltip may itself contain markup
|
||||
// such as src='...' or href='...'; that text is not a second HTML attribute.
|
||||
sanitized = sanitized.replace(/(<\/?[a-z][\w:-]*)([^>]*)(>)/gi, (_tag, opening: string, attributes: string, closing: string) => {
|
||||
const output: string[] = [opening];
|
||||
let copied = 0;
|
||||
for (const attribute of htmlAttributes(attributes)) {
|
||||
const { name, value, start, end } = attribute;
|
||||
let replacement: string | undefined;
|
||||
if (/^(?:on[\w:-]*|srcdoc|src|action|formaction|xlink:href)$/.test(name)) {
|
||||
removedContentCount += 1;
|
||||
replacement = '';
|
||||
} else if (name === 'href') {
|
||||
const target = decodeHtmlEntities(value ?? '').trim();
|
||||
if (isSafeLinkTarget(target)) replacement = `href="${escapeHtmlText(target)}"`;
|
||||
else { removedContentCount += 1; replacement = ''; }
|
||||
}
|
||||
if (replacement === undefined) continue;
|
||||
output.push(attributes.slice(copied, start), replacement);
|
||||
copied = end;
|
||||
}
|
||||
output.push(attributes.slice(copied), closing);
|
||||
return output.join('');
|
||||
});
|
||||
sanitized = sanitized.replace(/<!--[\s\S]*?-->/g, ' ');
|
||||
return { text: sanitized, removedContentCount };
|
||||
}
|
||||
|
||||
function attributeValue(attributes: string, name: string): string {
|
||||
const pattern = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(attributes))) {
|
||||
if (match[1]?.toLocaleLowerCase('en-US') === name.toLocaleLowerCase('en-US')) {
|
||||
return decodeHtmlEntities(match[2] ?? match[3] ?? match[4] ?? '').trim();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
return decodeHtmlEntities(htmlAttributeValue(attributes, name)).trim();
|
||||
}
|
||||
/**
|
||||
* Markiert der Bericht selbst diese Zeile als ausgewertet?
|
||||
|
|
@ -321,7 +326,7 @@ function isEvaluatedRowClass(classAttribute: string): boolean {
|
|||
|
||||
export function markupText(value: string): string {
|
||||
return normalizeInlineText(decodeHtmlEntities(
|
||||
value.replace(/<!--[\s\S]*?-->/g, ' ').replace(/<[^>]*>/g, ' ')
|
||||
protectHtmlAttributeMarkup(value, true).replace(/<!--[\s\S]*?-->/g, ' ').replace(/<[^>]*>/g, ' ')
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function formatAdapter(format: ImportFormat): ImportProcessorReference {
|
|||
return {
|
||||
kind: 'format-adapter',
|
||||
id: `${format.toLocaleLowerCase('en-US')}-format-adapter`,
|
||||
version: '1.0.0'
|
||||
version: format === 'HTML' ? '1.1.1' : format === 'XML' ? '1.1.0' : '1.0.0'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { ImportDiagnostic } from '../domain/import-intermediate';
|
||||
import type { EnvironmentObservationCandidate, ScannerReportSectionCandidate } from '../domain/source-extraction';
|
||||
import { isJsonObject } from './embedded-report-json';
|
||||
|
||||
const COUNTERS: Record<string, string> = { FailedCount: 'Failed', PassedCount: 'Passed', ErrorCount: 'Error', InvestigateCount: 'Investigate', SkippedCount: 'Skipped', NotRunCount: 'NotRun' };
|
||||
|
||||
export function maesterCountDiagnostics(root: Record<string, JsonValue>, path: string): ImportDiagnostic[] {
|
||||
const tests = Array.isArray(root.Tests) ? root.Tests : [];
|
||||
const counts = new Map<string, number>();
|
||||
for (const test of tests) {
|
||||
if (isJsonObject(test) && typeof test.Result === 'string') counts.set(test.Result, (counts.get(test.Result) ?? 0) + 1);
|
||||
}
|
||||
return Object.entries({ TotalCount: tests.length, ...Object.fromEntries(Object.entries(COUNTERS).map(([key, status]) => [key, counts.get(status) ?? 0])) })
|
||||
.flatMap(([key, actual]): ImportDiagnostic[] => {
|
||||
if (!(key in root)) return [];
|
||||
const declared = root[key];
|
||||
if (typeof declared !== 'number' || !Number.isSafeInteger(declared) || declared < 0) return [{
|
||||
severity: 'warning', code: 'maester-count-invalid', stage: 'extraction',
|
||||
message: `Die deklarierte Maester-Zahl ${key} ist keine gültige Testanzahl.`, details: { objectPath: `${path}.${key}` }
|
||||
}];
|
||||
return declared === actual ? [] : [{
|
||||
severity: 'warning', code: 'maester-count-mismatch', stage: 'extraction',
|
||||
message: `Maester ${key}: ${declared} deklariert, ${actual} aus Einzeltests gezählt. Originalwerte bleiben erhalten.`,
|
||||
details: { objectPath: `${path}.${key}`, counter: key, declared, actual }
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
const KNOWN: Record<string, { property: string; label: string; valueType?: EnvironmentObservationCandidate['valueType'] }> = {
|
||||
TenantId: { property: 'entra.tenant_id', label: 'Entra tenant ID' },
|
||||
TenantName: { property: 'entra.tenant_name', label: 'Entra tenant name' },
|
||||
ExecutedAt: { property: 'report.generated_at', label: 'Scan time', valueType: 'datetime' },
|
||||
CurrentVersion: { property: 'report.engine_version', label: 'Maester version' },
|
||||
TotalDuration: { property: 'maester.duration', label: 'Scan duration', valueType: 'duration' },
|
||||
Result: { property: 'maester.result', label: 'Reported overall result', valueType: 'enum' },
|
||||
TotalCount: { property: 'maester.tests.declared_total', label: 'Reported test count' },
|
||||
...Object.fromEntries(Object.entries(COUNTERS).map(([key, status]) => [key, { property: `maester.tests.declared_${status.toLowerCase()}`, label: `Reported ${status} count` }]))
|
||||
};
|
||||
|
||||
/** Preserve every non-finding root field, including future/unknown nested data. */
|
||||
export function maesterEnvironmentContext(fileName: string, root: Record<string, JsonValue>, path: string, ordinal: number) {
|
||||
const reportSections: ScannerReportSectionCandidate[] = [];
|
||||
const environmentObservations: EnvironmentObservationCandidate[] = [];
|
||||
const sectionKey = `maester-context-${ordinal}`;
|
||||
const subject = { type: 'maester-report', identity: typeof root.TenantId === 'string' ? root.TenantId : path };
|
||||
const observedAt = typeof root.ExecutedAt === 'string' && !Number.isNaN(Date.parse(root.ExecutedAt)) ? new Date(root.ExecutedAt).toISOString() : undefined;
|
||||
const entries = Object.entries(root).filter(([key]) => key !== 'Tests');
|
||||
if (!entries.length) return { reportSections, environmentObservations };
|
||||
reportSections.push({
|
||||
version: 1, sectionKey, label: 'Maester report context', ordinal, state: 'observed',
|
||||
tables: [{ name: 'Report metadata', headers: ['Property', 'Original value'], rows: entries.map(([key, value]) => [key, value]) }],
|
||||
source: { fileName, objectPath: path }, attributes: { kind: 'maester-report-context' }
|
||||
});
|
||||
for (const [key, value] of entries) {
|
||||
const known = KNOWN[key];
|
||||
const empty = value === null || value === '' || (Array.isArray(value) && value.length === 0);
|
||||
const inferredType = value !== null && typeof value === 'object' ? 'object' : typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string';
|
||||
environmentObservations.push({
|
||||
version: 1, subject, property: known?.property ?? `maester.source.${key}`, label: known?.label ?? key.replace(/([a-z])([A-Z])/g, '$1 $2'),
|
||||
originalValue: value, normalizedValue: value, valueType: known?.valueType ?? inferredType,
|
||||
state: empty ? 'empty' : 'observed', observedAt, sectionKey,
|
||||
source: { fileName, objectPath: `${path}.${key}` }, attributes: { sourceField: key, declared: key.endsWith('Count') }
|
||||
});
|
||||
}
|
||||
for (const [container, key, property, label] of [
|
||||
['SystemInfo', 'MachineName', 'maester.runner.host', 'Scanner host'],
|
||||
['SystemInfo', 'OSDescription', 'maester.runner.os', 'Scanner operating system'],
|
||||
['PowerShellInfo', 'Version', 'maester.powershell.version', 'PowerShell version']
|
||||
]) {
|
||||
const object = root[container];
|
||||
if (!isJsonObject(object) || !(key in object)) continue;
|
||||
const value = object[key];
|
||||
environmentObservations.push({
|
||||
version: 1, subject, property, label, originalValue: value, normalizedValue: value,
|
||||
valueType: value !== null && typeof value === 'object' ? 'object' : 'string',
|
||||
state: value === null || value === '' ? 'empty' : 'observed', observedAt, sectionKey,
|
||||
source: { fileName, objectPath: `${path}.${container}.${key}` }, attributes: { sourceField: `${container}.${key}`, scope: 'scanner-runtime' }
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(root.Tests)) return { reportSections, environmentObservations };
|
||||
// Computed counts are explicitly distinct from the report's declared figures.
|
||||
const tests = Array.isArray(root.Tests) ? root.Tests : [];
|
||||
for (const [key, status] of Object.entries({ total: '', ...Object.fromEntries(Object.values(COUNTERS).map(status => [status.toLowerCase(), status])) })) {
|
||||
const count = status ? tests.filter(test => isJsonObject(test) && test.Result === status).length : tests.length;
|
||||
environmentObservations.push({
|
||||
version: 1, subject, property: `maester.tests.actual_${key}`, label: `Counted ${key} tests`,
|
||||
originalValue: count, normalizedValue: count, valueType: 'number', state: 'observed', observedAt, sectionKey,
|
||||
source: { fileName, objectPath: `${path}.Tests` }, attributes: { derived: true, derivation: 'count-test-entries', status }
|
||||
});
|
||||
}
|
||||
return { reportSections, environmentObservations };
|
||||
}
|
||||
|
|
@ -1,37 +1,90 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { DecodedSourceDocument, ImportDiagnostic } from '../domain/import-intermediate';
|
||||
import type { FindingCandidate, SourceExtractor, SourceFieldCandidate } from '../domain/source-extraction';
|
||||
import { cellText, plainTextValue, stringifyCell } from '../shared/text';
|
||||
import { isJsonObject, readEmbeddedJsonObject } from './embedded-report-json';
|
||||
import { cellText, plainTextValue } from '../shared/text';
|
||||
import { isJsonObject } from './embedded-report-json';
|
||||
import { assignedReportObjects, readStaticReportObject, StaticReportLiteralError } from './static-report-literal';
|
||||
import { maesterEnvironmentContext, maesterCountDiagnostics } from './maester-environment-context';
|
||||
import { ImportError } from '../shared/errors';
|
||||
|
||||
const ASSIGNMENT = /(?:var|const|let)\s+[$A-Za-z_][\w$]*\s*=\s*/g;
|
||||
const MAX_TESTS = 250_000;
|
||||
|
||||
interface MaesterPayload {
|
||||
roots: Array<{ value: Record<string, JsonValue>; path: string }>;
|
||||
envelope?: Record<string, JsonValue>;
|
||||
recognized: boolean;
|
||||
diagnostics: ImportDiagnostic[];
|
||||
}
|
||||
|
||||
const payloadCache = new WeakMap<DecodedSourceDocument, MaesterPayload>();
|
||||
const REPORT_START = /^\{\s*["']?(?:Result|Tests|Tenants|TenantId|TenantName|TotalCount|FailedCount|ExecutedAt|MaesterConfig)["']?\s*:/;
|
||||
const REPORT_SHAPE = /["']?(?:Tests|Tenants|FailedCount|MaesterConfig)["']?\s*:/;
|
||||
|
||||
function* reportObjects(script: string) {
|
||||
try { yield* assignedReportObjects(script); }
|
||||
catch (error) {
|
||||
if (!(error instanceof StaticReportLiteralError)) throw error;
|
||||
throw new ImportError('Die eingebettete Scriptstruktur überschreitet die sichere Lesegrenze.', [{
|
||||
severity: 'error', code: 'maester-script-limit', stage: 'extraction', message: 'Die eingebettete Scriptstruktur ist zu tief verschachtelt.'
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
function maesterPayload(document: DecodedSourceDocument): MaesterPayload {
|
||||
if (document.format !== 'HTML') return { roots: [], recognized: false, diagnostics: [] };
|
||||
for (const match of document.rawText.matchAll(ASSIGNMENT)) {
|
||||
const start = document.rawText.indexOf('{', match.index! + match[0].length);
|
||||
if (start < 0) continue;
|
||||
const embedded = readEmbeddedJsonObject(document.rawText, start);
|
||||
if (!embedded || !isJsonObject(embedded.value)) continue;
|
||||
const value = embedded.value;
|
||||
if (Array.isArray(value.Tenants)) {
|
||||
const roots = value.Tenants
|
||||
.map((tenant, index) => isJsonObject(tenant) ? { value: tenant, path: `$.Tenants[${index}]` } : undefined)
|
||||
.filter((item): item is { value: Record<string, JsonValue>; path: string } => Boolean(item));
|
||||
return { roots, recognized: true, diagnostics: [] };
|
||||
}
|
||||
if (Array.isArray(value.Tests) && ('Result' in value || 'EndOfJson' in value)) {
|
||||
return { roots: [{ value, path: '$' }], recognized: true, diagnostics: [] };
|
||||
const cached = payloadCache.get(document);
|
||||
if (cached) return cached;
|
||||
const result: MaesterPayload = { roots: [], recognized: false, diagnostics: [] };
|
||||
let attempts = 0;
|
||||
for (const script of document.rawText.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi)) {
|
||||
const text = script[2];
|
||||
const starts = /\btype\s*=\s*["']application\/json["']/i.test(script[1])
|
||||
? [{ start: text.search(/\S/), binding: 'json-data' }]
|
||||
: reportObjects(text);
|
||||
let consumedUntil = -1;
|
||||
for (const { start, binding } of starts) {
|
||||
const reportBinding = /^(?:testResults|maesterData|maesterReportData)$/i.test(binding);
|
||||
const recognizable = reportBinding || REPORT_SHAPE.test(text.slice(start, start + 512));
|
||||
if (start < consumedUntil || (!reportBinding && !REPORT_START.test(text.slice(start, start + 512)))) continue;
|
||||
if (++attempts > 32) {
|
||||
result.recognized = true;
|
||||
result.diagnostics.push({ severity: 'error', code: 'maester-payload-limit', stage: 'extraction', message: 'Zu viele mögliche Maester-Datenobjekte im Bericht.' });
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const embedded = readStaticReportObject(text, start);
|
||||
consumedUntil = embedded.end;
|
||||
const value = embedded.value;
|
||||
if (Array.isArray(value.Tenants)) {
|
||||
result.recognized = true;
|
||||
if (result.roots.length) throw new StaticReportLiteralError('Ambiguous report roots.');
|
||||
result.envelope = Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'Tenants'));
|
||||
for (const [index, tenant] of value.Tenants.entries()) {
|
||||
if (!isJsonObject(tenant) || !Array.isArray(tenant.Tests)) throw new StaticReportLiteralError('Invalid tenant tests.');
|
||||
result.roots.push({ value: tenant, path: `$.Tenants[${index}]` });
|
||||
}
|
||||
} else if (Array.isArray(value.Tests) && ('Result' in value || 'EndOfJson' in value)) {
|
||||
result.recognized = true;
|
||||
if (result.roots.length) throw new StaticReportLiteralError('Ambiguous report roots.');
|
||||
result.roots.push({ value, path: '$' });
|
||||
} else if (recognizable) throw new StaticReportLiteralError('Invalid report shape.');
|
||||
} catch (error) {
|
||||
if (!(error instanceof StaticReportLiteralError)) throw error;
|
||||
if (recognizable) {
|
||||
result.recognized = true;
|
||||
result.diagnostics.push({ severity: 'error', code: 'maester-payload-invalid', stage: 'extraction',
|
||||
message: 'Maester-Daten erkannt, aber nicht sicher vollständig lesbar. Nur statische Datenobjekte ohne Ausdrücke sind zulässig; beschädigte, mehrdeutige oder zu große Berichte werden nicht importiert.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { roots: [], recognized: false, diagnostics: [] };
|
||||
if (!result.recognized && /maester/i.test(document.rawText) && /\b(?:testResults|maesterData)\s*=/.test(document.rawText)) {
|
||||
result.recognized = true;
|
||||
result.diagnostics.push({ severity: 'error', code: 'maester-payload-invalid', stage: 'extraction',
|
||||
message: 'Ein Maester-Bericht wurde erkannt, seine Ergebnisdaten liegen aber nicht als unterstütztes statisches Datenobjekt vor.' });
|
||||
}
|
||||
payloadCache.set(document, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function field(
|
||||
|
|
@ -49,7 +102,7 @@ function optionalField(
|
|||
fields: SourceFieldCandidate[], fileName: string, path: string, key: string, label: string,
|
||||
value: JsonValue | undefined, decodedValue?: string
|
||||
): void {
|
||||
if (value === undefined || value === null || stringifyCell(value).trim() === '') return;
|
||||
if (value === undefined) return;
|
||||
fields.push(field(fileName, path, key, label, value, decodedValue));
|
||||
}
|
||||
|
||||
|
|
@ -57,9 +110,15 @@ function testCandidate(fileName: string, test: Record<string, JsonValue>, path:
|
|||
const detail = isJsonObject(test.ResultDetail) ? test.ResultDetail : {};
|
||||
const fields: SourceFieldCandidate[] = [];
|
||||
optionalField(fields, fileName, path, 'Id', 'ID', test.Id);
|
||||
optionalField(fields, fileName, path, 'Title', 'Title', test.Title ?? test.Name);
|
||||
optionalField(fields, fileName, path, 'Title', 'Title', test.Title);
|
||||
if (test.Title === undefined || test.Title === null || test.Title === '') {
|
||||
optionalField(fields, fileName, path, 'Name', 'Title', test.Name);
|
||||
}
|
||||
optionalField(fields, fileName, path, 'Result', 'Result', test.Result);
|
||||
optionalField(fields, fileName, path, 'Severity', 'Severity', test.Severity ?? detail.Severity);
|
||||
optionalField(fields, fileName, path, 'Severity', 'Severity', test.Severity);
|
||||
if ((test.Severity === undefined || test.Severity === null || test.Severity === '') && detail.Severity !== undefined) {
|
||||
optionalField(fields, fileName, `${path}.ResultDetail`, 'Severity', 'Severity', detail.Severity);
|
||||
}
|
||||
optionalField(fields, fileName, path, 'Tag', 'Tag', test.Tag);
|
||||
optionalField(fields, fileName, path, 'Block', 'Block', test.Block);
|
||||
optionalField(fields, fileName, path, 'Duration', 'Duration', test.Duration);
|
||||
|
|
@ -75,37 +134,65 @@ function testCandidate(fileName: string, test: Record<string, JsonValue>, path:
|
|||
optionalField(fields, fileName, path, 'ScriptBlockFile', 'TestScriptFile', test.ScriptBlockFile);
|
||||
optionalField(fields, fileName, path, 'ScriptBlock', 'TestScript', test.ScriptBlock);
|
||||
optionalField(fields, fileName, `${path}.ResultDetail`, 'Service', 'Service', detail.Service);
|
||||
// Keep future/unmapped fields and their exact object paths, not just known labels.
|
||||
const known = new Set(['Id', 'Title', 'Result', 'Severity', 'Tag', 'Block', 'Duration', 'ErrorRecord', 'Name', 'HelpUrl', 'ScriptBlockFile', 'ScriptBlock', 'ResultDetail']);
|
||||
for (const [key, value] of Object.entries(test)) {
|
||||
if (!known.has(key)) fields.push(field(fileName, path, key, key, value));
|
||||
}
|
||||
const knownDetails = new Set(['TestDescription', 'TestResult', 'TestSkipped', 'SkippedReason', 'Service']);
|
||||
for (const [key, value] of Object.entries(detail)) {
|
||||
if (!knownDetails.has(key)) fields.push(field(fileName, `${path}.ResultDetail`, key, `ResultDetail.${key}`, value));
|
||||
}
|
||||
if (test.ResultDetail !== undefined && !isJsonObject(test.ResultDetail)) {
|
||||
fields.push(field(fileName, path, 'ResultDetail', 'ResultDetail', test.ResultDetail));
|
||||
}
|
||||
return { version: 1, id: `maester-test-${index}`, source: { fileName, objectPath: path }, fields };
|
||||
}
|
||||
|
||||
export const maesterHtmlSourceExtractor: SourceExtractor = {
|
||||
id: 'maester-html-source-extractor',
|
||||
version: '1.0.0',
|
||||
version: '1.1.0',
|
||||
detect(document) {
|
||||
const payload = maesterPayload(document);
|
||||
const tests = payload.roots.reduce((count, root) => count + (Array.isArray(root.value.Tests) ? root.value.Tests.length : 0), 0);
|
||||
return payload.recognized && tests > 0
|
||||
? { score: 1, evidence: [`Sicher eingebettetes Maester-Ergebnisobjekt mit ${tests} Testeinträgen erkannt.`] }
|
||||
return payload.recognized && (tests > 0 || payload.diagnostics.length > 0)
|
||||
? { score: 1, toolId: 'maester', evidence: [`Strukturiertes Maester-Ergebnisobjekt mit ${tests} Testeinträgen erkannt.`] }
|
||||
: { score: 0, evidence: [] };
|
||||
},
|
||||
extract(document) {
|
||||
const payload = maesterPayload(document);
|
||||
if (payload.diagnostics.some(item => item.severity === 'error')) {
|
||||
throw new ImportError('Maester-HTML konnte nicht sicher vollständig gelesen werden.', payload.diagnostics);
|
||||
}
|
||||
const total = payload.roots.reduce((sum, root) => sum + (Array.isArray(root.value.Tests) ? root.value.Tests.length : 0), 0);
|
||||
if (total > MAX_TESTS) throw new ImportError('Maester-Testlimit überschritten.', [{
|
||||
severity: 'error', code: 'maester-test-limit', stage: 'extraction', message: `Mehr als ${MAX_TESTS} Tests. Der Bericht wird nicht still gekürzt.`
|
||||
}]);
|
||||
const candidates: FindingCandidate[] = [];
|
||||
const diagnostics = [...payload.diagnostics];
|
||||
const reportSections: NonNullable<ReturnType<typeof maesterEnvironmentContext>['reportSections']> = [];
|
||||
const environmentObservations: NonNullable<ReturnType<typeof maesterEnvironmentContext>['environmentObservations']> = [];
|
||||
if (payload.envelope) {
|
||||
const context = maesterEnvironmentContext(document.file.name, payload.envelope, '$', 0);
|
||||
reportSections.push(...context.reportSections);
|
||||
environmentObservations.push(...context.environmentObservations);
|
||||
}
|
||||
let sourceIndex = 0;
|
||||
for (const root of payload.roots) {
|
||||
const tests = Array.isArray(root.value.Tests) ? root.value.Tests : [];
|
||||
diagnostics.push(...maesterCountDiagnostics(root.value, root.path));
|
||||
const context = maesterEnvironmentContext(document.file.name, root.value, root.path, reportSections.length);
|
||||
reportSections.push(...context.reportSections);
|
||||
environmentObservations.push(...context.environmentObservations);
|
||||
for (let index = 0; index < tests.length && candidates.length < MAX_TESTS; index += 1) {
|
||||
const test = tests[index];
|
||||
if (!isJsonObject(test) || (!test.Id && !test.Title && !test.Name)) continue;
|
||||
if (!isJsonObject(test) || (!test.Id && !test.Title && !test.Name)) {
|
||||
throw new ImportError('Maester enthält einen ungültigen Testeintrag.', [{ severity: 'error', code: 'maester-test-invalid', stage: 'extraction', message: 'Ein Testeintrag besitzt keine lesbare Identität.', details: { objectPath: `${root.path}.Tests[${index}]` } }]);
|
||||
}
|
||||
const path = `${root.path}.Tests[${index}]`;
|
||||
candidates.push(testCandidate(document.file.name, test, path, sourceIndex++));
|
||||
}
|
||||
}
|
||||
const diagnostics = [...payload.diagnostics];
|
||||
if (candidates.length >= MAX_TESTS) diagnostics.push({
|
||||
severity: 'warning', code: 'maester-test-limit', stage: 'extraction',
|
||||
message: `Der Maester-Bericht wurde auf ${MAX_TESTS} Testeinträge begrenzt.`
|
||||
});
|
||||
return { version: 1, candidates, diagnostics };
|
||||
return { version: 1, candidates, diagnostics, reportSections, environmentObservations };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import type { ImportDiagnostic, SourceLocator } from '../domain/import-intermediate';
|
||||
import type { FindingCandidate, ScannerReportSectionCandidate } from '../domain/source-extraction';
|
||||
import { ImportError } from '../shared/errors';
|
||||
import { markupText } from './html-extraction';
|
||||
|
||||
export function acceptPingCastleRuleId(ruleId: string, seen: Set<string>, source: SourceLocator, diagnostics: ImportDiagnostic[]): boolean {
|
||||
if (!ruleId.trim()) throw new ImportError('PingCastle-Regel ohne Rule ID kann nicht vollständig importiert werden.', [{
|
||||
severity: 'error', code: 'pingcastle-rule-id-missing', stage: 'extraction', source,
|
||||
message: 'Ein nativer Regelblock enthält keine lesbare Rule ID.'
|
||||
}]);
|
||||
const identity = ruleId.trim().toLocaleUpperCase('en-US');
|
||||
if (seen.has(identity)) {
|
||||
diagnostics.push({
|
||||
severity: 'warning', code: 'pingcastle-duplicate-rule-id', stage: 'extraction', source,
|
||||
message: 'Eine Rule ID ist innerhalb der Quelle wiederholt. Die erste Darstellung bleibt das Finding; weitere Darstellungen werden nicht als zusätzliche Treffer gezählt.',
|
||||
details: { ruleId, retained: 'first-presentation' }
|
||||
});
|
||||
return false;
|
||||
}
|
||||
seen.add(identity);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Matched counts describe findings; the adjacent catalog size is not a result count. */
|
||||
export function pingCastleHtmlRuleCoverage(fileName: string, markup: string, candidates: FindingCandidate[]): {
|
||||
diagnostics: ImportDiagnostic[]; section?: ScannerReportSectionCandidate;
|
||||
} {
|
||||
const diagnostics: ImportDiagnostic[] = [];
|
||||
const rows: Array<Array<string | number>> = [];
|
||||
const key = (value: string) => value.replace(/[^a-z]/gi, '').toLocaleLowerCase('en-US');
|
||||
const actualByCategory = new Map<string, number>();
|
||||
for (const candidate of candidates) {
|
||||
const category = key(candidate.fields.find(field => field.label === 'Category')?.decodedValue ?? '');
|
||||
actualByCategory.set(category, (actualByCategory.get(category) ?? 0) + 1);
|
||||
}
|
||||
const pattern = /<h2\b([^>]*)>([\s\S]*?)<\/h2\s*>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
let positiveDeclared = false;
|
||||
while ((match = pattern.exec(markup))) {
|
||||
if (!/\bclass\s*=\s*["'][^"']*\bindicator-header\b/i.test(match[1])) continue;
|
||||
const heading = markupText(match[2]);
|
||||
if (!/rules?\s+matched/i.test(heading)) continue;
|
||||
const source = { fileName, objectPath: `html/indicator-headings/${rows.length}` };
|
||||
const parsed = /^(.*?)\s+rule\s+details\s*\[\s*(\S+)\s+rules?\s+matched\s+on\s+a\s+total\s+of\s+(\S+)\s*\]/i.exec(heading);
|
||||
const category = parsed?.[1] ?? heading;
|
||||
const declared = parsed && /^\d+$/.test(parsed[2]) ? Number(parsed[2]) : NaN;
|
||||
const catalog = parsed && /^\d+$/.test(parsed[3]) ? Number(parsed[3]) : NaN;
|
||||
const actual = actualByCategory.get(key(category)) ?? 0;
|
||||
rows.push([category, parsed?.[2] ?? '', parsed?.[3] ?? '', actual, heading]);
|
||||
positiveDeclared ||= Number.isFinite(declared) && declared > 0;
|
||||
if (!Number.isSafeInteger(declared) || !Number.isSafeInteger(catalog) || declared > catalog) {
|
||||
diagnostics.push({ severity: 'warning', code: 'pingcastle-rule-count-invalid', stage: 'extraction', source,
|
||||
message: 'Die deklarierte PingCastle-Regelbilanz ist ungültig; Originalangaben bleiben erhalten.' });
|
||||
}
|
||||
if (Number.isSafeInteger(declared) && declared !== actual) {
|
||||
diagnostics.push({ severity: 'warning', code: 'pingcastle-rule-count-mismatch', stage: 'extraction', source,
|
||||
message: 'Die deklarierte Trefferzahl stimmt nicht mit den extrahierten eindeutigen Regeln überein.',
|
||||
details: { category, declared, actual } });
|
||||
}
|
||||
}
|
||||
if (!candidates.length && (positiveDeclared || diagnostics.length)) throw new ImportError('PingCastle kündigt Regeln an, enthält aber keine auslesbaren Ergebnisse.', [
|
||||
...diagnostics, { severity: 'error', code: 'pingcastle-rule-results-missing', stage: 'extraction',
|
||||
message: 'Eine erfolgreiche Nullbefund-Verarbeitung ist bei dieser Regelbilanz nicht zulässig.', source: { fileName } }
|
||||
]);
|
||||
return { diagnostics, ...(rows.length ? { section: {
|
||||
version: 1 as const, sectionKey: 'pingcastle-rule-coverage', label: 'PingCastle rule coverage', ordinal: 0,
|
||||
state: 'observed' as const,
|
||||
tables: [{ name: 'Declared and extracted rules', headers: ['Category', 'Declared matched', 'Declared catalog size', 'Extracted unique rules', 'Original heading'], rows }],
|
||||
source: { fileName, objectPath: 'html/indicator-headings' }, attributes: { derivedColumn: 'Extracted unique rules' }
|
||||
} } : {}) };
|
||||
}
|
||||
|
|
@ -14,6 +14,10 @@ import type {
|
|||
} from '../domain/source-extraction';
|
||||
import type { ImportTable } from '../domain/import-intermediate';
|
||||
import { findHtmlTableSpans, markupText, sanitizeHtmlText } from './html-extraction';
|
||||
import { protectHtmlAttributeMarkup } from './html-attribute-markup';
|
||||
import { htmlAttributeValue as attributeValue } from './html-attributes';
|
||||
import { indexHtmlElements, type HtmlElementIndex } from './html-element-index';
|
||||
import { acceptPingCastleRuleId, pingCastleHtmlRuleCoverage } from './pingcastle-rule-validation';
|
||||
|
||||
const maximumRuleCandidates = 5_000;
|
||||
const maximumFieldMarkupCharacters = 1_000_000;
|
||||
|
|
@ -199,17 +203,11 @@ function proseText(markup: string): string {
|
|||
const subSectionEndMarker = '<i data-subsection-end="1"></i>';
|
||||
const subSectionStartMarker = '<i data-subsection-start="1"></i>';
|
||||
|
||||
function subsectionMarkup(markup: string, anchorId: string): string | null {
|
||||
const anchorPattern = /<a\b([^>]*)>/gi;
|
||||
let anchorMatch: RegExpExecArray | null;
|
||||
let afterAnchor = -1;
|
||||
while ((anchorMatch = anchorPattern.exec(markup))) {
|
||||
if (attributeValue(anchorMatch[1] ?? '', 'name') !== anchorId) continue;
|
||||
const closing = /^\s*<\/a\s*>/i.exec(markup.slice(anchorPattern.lastIndex, anchorPattern.lastIndex + 256));
|
||||
afterAnchor = anchorPattern.lastIndex + (closing?.[0].length ?? 0);
|
||||
break;
|
||||
}
|
||||
if (afterAnchor < 0) return null;
|
||||
function subsectionMarkup(markup: string, anchorId: string, index: HtmlElementIndex): string | null {
|
||||
const anchor = index.namedAnchors.get(anchorId);
|
||||
if (!anchor) return null;
|
||||
const closing = /^\s*<\/a\s*>/i.exec(markup.slice(anchor.bodyStart, anchor.bodyStart + 256));
|
||||
const afterAnchor = anchor.bodyStart + (closing?.[0].length ?? 0);
|
||||
const boundedEnd = Math.min(markup.length, afterAnchor + maximumAnchorSectionCharacters);
|
||||
const fragment = markup.slice(afterAnchor, boundedEnd);
|
||||
const ownEndMarkerIndex = fragment.indexOf(subSectionEndMarker);
|
||||
|
|
@ -221,64 +219,23 @@ function subsectionMarkup(markup: string, anchorId: string): string | null {
|
|||
return nextSectionIndex >= 0 ? rest.slice(0, nextSectionIndex) : rest;
|
||||
}
|
||||
|
||||
function attributeValue(attributes: string, name: string): string {
|
||||
const pattern = /([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(attributes))) {
|
||||
if (match[1]?.toLocaleLowerCase('en-US') === name.toLocaleLowerCase('en-US')) {
|
||||
return match[2] ?? match[3] ?? match[4] ?? '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function elementSpanById(markup: string, targetId: string): ElementSpan | null {
|
||||
const openingPattern = /<([a-zA-Z][\w:-]*)\b([^>]*)>/g;
|
||||
let opening: RegExpExecArray | null;
|
||||
while ((opening = openingPattern.exec(markup))) {
|
||||
if (attributeValue(opening[2] ?? '', 'id') !== targetId) continue;
|
||||
const tagName = opening[1];
|
||||
const start = opening.index;
|
||||
const bodyStart = openingPattern.lastIndex;
|
||||
if (/\/\s*>$/.test(opening[0])) return { start, bodyStart, bodyEnd: bodyStart, end: bodyStart };
|
||||
const tagPattern = new RegExp('<' + escapeRegExp(tagName) + '\\b[^>]*>|<\\/' + escapeRegExp(tagName) + '\\s*>', 'gi');
|
||||
tagPattern.lastIndex = bodyStart;
|
||||
let depth = 1;
|
||||
let tag: RegExpExecArray | null;
|
||||
while ((tag = tagPattern.exec(markup))) {
|
||||
if (/^<\//.test(tag[0])) depth -= 1;
|
||||
else if (!/\/\s*>$/.test(tag[0])) depth += 1;
|
||||
if (depth === 0) return { start, bodyStart, bodyEnd: tag.index, end: tagPattern.lastIndex };
|
||||
}
|
||||
return null;
|
||||
function elementSpanById(markup: string, targetId: string, index: HtmlElementIndex): ElementSpan | null {
|
||||
const opening = index.byId.get(targetId);
|
||||
if (!opening) return null;
|
||||
const { tagName, start, bodyStart, selfClosing } = opening;
|
||||
if (selfClosing) return { start, bodyStart, bodyEnd: bodyStart, end: bodyStart };
|
||||
const tagPattern = new RegExp('<' + escapeRegExp(tagName) + '\\b[^>]*>|<\\/' + escapeRegExp(tagName) + '\\s*>', 'gi');
|
||||
tagPattern.lastIndex = bodyStart;
|
||||
let depth = 1;
|
||||
let tag: RegExpExecArray | null;
|
||||
while ((tag = tagPattern.exec(markup))) {
|
||||
if (/^<\//.test(tag[0])) depth -= 1;
|
||||
else if (!/\/\s*>$/.test(tag[0])) depth += 1;
|
||||
if (depth === 0) return { start, bodyStart, bodyEnd: tag.index, end: tagPattern.lastIndex };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function targetDefinitionCount(markup: string, targetId: string): number {
|
||||
let count = 0;
|
||||
const openingPattern = /<([a-zA-Z][\w:-]*)\b([^>]*)>/g;
|
||||
let opening: RegExpExecArray | null;
|
||||
while ((opening = openingPattern.exec(markup))) {
|
||||
const attributes = opening[2] ?? '';
|
||||
if (attributeValue(attributes, 'id') === targetId || attributeValue(attributes, 'name') === targetId) count += 1;
|
||||
if (count > 1) break;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function targetDefinitionIndex(markup: string, targetId: string): number {
|
||||
const openingPattern = /<([a-zA-Z][\w:-]*)\b([^>]*)>/g;
|
||||
let opening: RegExpExecArray | null;
|
||||
while ((opening = openingPattern.exec(markup))) {
|
||||
const attributes = opening[2] ?? '';
|
||||
if (attributeValue(attributes, 'id') === targetId || attributeValue(attributes, 'name') === targetId) {
|
||||
return opening.index;
|
||||
}
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function referenceLabelAt(markup: string, markerEnd: number, fallback: string): string {
|
||||
const bounded = markup.slice(markerEnd, Math.min(markup.length, markerEnd + 4_096));
|
||||
const closing = /<\/(?:a|button)\s*>/i.exec(bounded);
|
||||
|
|
@ -307,10 +264,10 @@ function internalReferences(markup: string): InternalReference[] {
|
|||
return references;
|
||||
}
|
||||
|
||||
function targetMarkup(markup: string, targetId: string): { markup: string; label: string } | null {
|
||||
const subsection = subsectionMarkup(markup, targetId);
|
||||
function targetMarkup(markup: string, targetId: string, index: HtmlElementIndex): { markup: string; label: string } | null {
|
||||
const subsection = subsectionMarkup(markup, targetId, index);
|
||||
if (subsection !== null) return { markup: subsection, label: targetId };
|
||||
const span = elementSpanById(markup, targetId);
|
||||
const span = elementSpanById(markup, targetId, index);
|
||||
if (!span) return null;
|
||||
const body = markup.slice(span.bodyStart, span.bodyEnd);
|
||||
const title = /<h[1-6]\b[^>]*class\s*=\s*(?:"[^"]*\bmodal-title\b[^"]*"|'[^']*\bmodal-title\b[^']*')[^>]*>([\s\S]*?)<\/h[1-6]\s*>/i.exec(body);
|
||||
|
|
@ -336,7 +293,8 @@ function maskRanges(value: string, ranges: ElementSpan[]): string {
|
|||
|
||||
function resolveLinkedSections(
|
||||
markup: string,
|
||||
roots: InternalReference[]
|
||||
roots: InternalReference[],
|
||||
elementIndex: HtmlElementIndex
|
||||
): { sections: ResolvedAnchorSection[]; text: string; issues: LinkResolutionIssue[] } {
|
||||
const sections: ResolvedAnchorSection[] = [];
|
||||
const textBlocks: string[] = [];
|
||||
|
|
@ -361,7 +319,7 @@ function resolveLinkedSections(
|
|||
}
|
||||
if (visited.has(reference.targetId)) return;
|
||||
visited.add(reference.targetId);
|
||||
const definitionCount = targetDefinitionCount(markup, reference.targetId);
|
||||
const definitionCount = elementIndex.definitions.get(reference.targetId)?.count ?? 0;
|
||||
if (definitionCount === 0) {
|
||||
addIssue('unresolved', reference.targetId);
|
||||
return;
|
||||
|
|
@ -370,7 +328,7 @@ function resolveLinkedSections(
|
|||
addIssue('ambiguous', reference.targetId);
|
||||
return;
|
||||
}
|
||||
const target = targetMarkup(markup, reference.targetId);
|
||||
const target = targetMarkup(markup, reference.targetId, elementIndex);
|
||||
if (!target) {
|
||||
addIssue('unresolved', reference.targetId);
|
||||
return;
|
||||
|
|
@ -378,8 +336,9 @@ function resolveLinkedSections(
|
|||
const label = target.label === reference.targetId ? reference.label : target.label;
|
||||
const sectionLabel = parentLabel ? parentLabel + ' → ' + label : label;
|
||||
const allReferences = internalReferences(target.markup);
|
||||
const targetIndex = indexHtmlElements(target.markup);
|
||||
const referencedSpans = allReferences
|
||||
.map(child => ({ child, span: elementSpanById(target.markup, child.targetId) }))
|
||||
.map(child => ({ child, span: elementSpanById(target.markup, child.targetId, targetIndex) }))
|
||||
.filter((entry): entry is { child: InternalReference; span: ElementSpan } => Boolean(entry.span));
|
||||
const directReferences = allReferences.filter(candidate => !referencedSpans.some(entry =>
|
||||
entry.child.targetId !== candidate.targetId
|
||||
|
|
@ -426,7 +385,8 @@ function resolveLinkedSections(
|
|||
// Reihenfolge der tatsaechlichen Zielabschnitte im Bericht, nicht die
|
||||
// zufaellige Position ihrer Ausloeser.
|
||||
directReferences
|
||||
.sort((left, right) => targetDefinitionIndex(markup, left.targetId) - targetDefinitionIndex(markup, right.targetId)
|
||||
.sort((left, right) => (elementIndex.definitions.get(left.targetId)?.start ?? Number.MAX_SAFE_INTEGER)
|
||||
- (elementIndex.definitions.get(right.targetId)?.start ?? Number.MAX_SAFE_INTEGER)
|
||||
|| left.index - right.index)
|
||||
.forEach(child => visit(child, sectionLabel, depth + 1));
|
||||
};
|
||||
|
|
@ -443,6 +403,7 @@ function htmlLabels(markup: string): {
|
|||
const labels: HtmlLabel[] = [];
|
||||
const detailsExtras = new Map<number, DetailsExtras>();
|
||||
const linkIssues: LinkResolutionIssue[] = [];
|
||||
const elementIndex = indexHtmlElements(markup);
|
||||
const pattern = /<strong\b[^>]*>([\s\S]{0,2048}?)<\/strong\s*>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = pattern.exec(markup)) && labels.length < maximumRuleCandidates * 16) {
|
||||
|
|
@ -460,7 +421,7 @@ function htmlLabels(markup: string): {
|
|||
const embeddedText = embedded.map(row => row.join(' | ')).join('\n');
|
||||
value = value ? `${value}\n${embeddedText}` : embeddedText;
|
||||
}
|
||||
const resolved = resolveLinkedSections(markup, internalReferences(paragraph.rawValue));
|
||||
const resolved = resolveLinkedSections(markup, internalReferences(paragraph.rawValue), elementIndex);
|
||||
linkIssues.push(...resolved.issues);
|
||||
if (resolved.text) value = value ? `${value}\n${resolved.text}` : resolved.text;
|
||||
if (embedded.length > 1 || resolved.sections.length) {
|
||||
|
|
@ -589,7 +550,7 @@ function htmlCandidates(document: DecodedSourceDocument): {
|
|||
reportSections: ScannerReportSectionCandidate[];
|
||||
environmentObservations: EnvironmentObservationCandidate[];
|
||||
} {
|
||||
const preprocessed = preserveSubSectionMarkers(preserveInternalAnchorHrefs(preserveInternalReferences(document.rawText)));
|
||||
const preprocessed = preserveSubSectionMarkers(preserveInternalAnchorHrefs(preserveInternalReferences(protectHtmlAttributeMarkup(document.rawText))));
|
||||
const markup = sanitizeHtmlText(preprocessed).text;
|
||||
const reportText = markupText(markup.slice(0, Math.min(markup.length, 500_000)));
|
||||
const scoreValue = (pattern: RegExp) => pattern.exec(reportText)?.[1] ?? '';
|
||||
|
|
@ -601,15 +562,14 @@ function htmlCandidates(document: DecodedSourceDocument): {
|
|||
anomalies: { value: scoreValue(/Anomalies\s*:\s*(\d+)\s*\/\s*100/i), source: { fileName: document.file.name, objectPath: 'html/risk-score/anomalies' } }
|
||||
};
|
||||
const { labels, detailsExtras, linkIssues } = htmlLabels(markup);
|
||||
const ruleLabels = labels.filter(item => item.label === 'rule id' && item.value);
|
||||
const ruleLabels = labels.filter(item => item.label === 'rule id');
|
||||
const candidates: FindingCandidate[] = [];
|
||||
const ruleDiagnostics: ImportDiagnostic[] = [];
|
||||
const seenRuleIds = new Set<string>();
|
||||
for (let ruleIndex = 0; ruleIndex < ruleLabels.length && candidates.length < maximumRuleCandidates; ruleIndex += 1) {
|
||||
const rule = ruleLabels[ruleIndex];
|
||||
const ruleId = rule.value.trim();
|
||||
const identity = ruleId.toLocaleUpperCase('en-US');
|
||||
if (!identity || seenRuleIds.has(identity)) continue;
|
||||
seenRuleIds.add(identity);
|
||||
if (!acceptPingCastleRuleId(ruleId, seenRuleIds, { fileName: document.file.name, objectPath: `html/rule-blocks/${ruleIndex}` }, ruleDiagnostics)) continue;
|
||||
const nextRuleIndex = ruleLabels[ruleIndex + 1]?.index ?? markup.length;
|
||||
const blockLabels = labels.filter(item => item.index >= rule.index && item.index < nextRuleIndex);
|
||||
const firstValue = (...names: string[]) =>
|
||||
|
|
@ -681,11 +641,14 @@ function htmlCandidates(document: DecodedSourceDocument): {
|
|||
stage: 'extraction'
|
||||
});
|
||||
const sections = reportSections(document);
|
||||
const coverage = pingCastleHtmlRuleCoverage(document.file.name, markup, candidates);
|
||||
// Coverage is source metadata, not another set of findings or environment assets.
|
||||
if (coverage.section) sections.push({ ...coverage.section, ordinal: sections.length });
|
||||
return {
|
||||
candidates,
|
||||
diagnostics,
|
||||
diagnostics: [...diagnostics, ...ruleDiagnostics, ...coverage.diagnostics],
|
||||
reportSections: sections,
|
||||
environmentObservations: contextObservations(document, sections, reportScores)
|
||||
environmentObservations: contextObservations(document, sections.filter(section => section.sectionKey !== 'pingcastle-rule-coverage'), reportScores)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -696,7 +659,7 @@ const excludedFindingSections = new Set([
|
|||
'sectionAnomaliesanalysis'
|
||||
]);
|
||||
|
||||
function sectionSpans(markup: string, includeExcluded = false): NamedSectionSpan[] {
|
||||
function sectionSpans(markup: string, includeExcluded = false, elementIndex = indexHtmlElements(markup)): NamedSectionSpan[] {
|
||||
const candidates: NamedSectionSpan[] = [];
|
||||
const pattern = /<div\b([^>]*)>/gi;
|
||||
let match: RegExpExecArray | null;
|
||||
|
|
@ -704,7 +667,7 @@ function sectionSpans(markup: string, includeExcluded = false): NamedSectionSpan
|
|||
while ((match = pattern.exec(markup))) {
|
||||
const sectionKey = attributeValue(match[1] ?? '', 'id');
|
||||
if (!/^section[a-zA-Z0-9_.:-]+$/i.test(sectionKey) || seen.has(sectionKey)) continue;
|
||||
const span = elementSpanById(markup, sectionKey);
|
||||
const span = elementSpanById(markup, sectionKey, elementIndex);
|
||||
if (!span) continue;
|
||||
seen.add(sectionKey);
|
||||
candidates.push({ ...span, sectionKey });
|
||||
|
|
@ -747,8 +710,9 @@ function sectionState(prose: string, tables: ScannerReportSectionCandidate['tabl
|
|||
|
||||
function reportSections(document: DecodedSourceDocument): ScannerReportSectionCandidate[] {
|
||||
const markup = sanitizeHtmlText(document.rawText).text;
|
||||
const sections = sectionSpans(markup);
|
||||
const allSections = sectionSpans(markup, true);
|
||||
const elementIndex = indexHtmlElements(markup);
|
||||
const sections = sectionSpans(markup, false, elementIndex);
|
||||
const allSections = sectionSpans(markup, true, elementIndex);
|
||||
const tableSpans = findHtmlTableSpans(markup);
|
||||
const rootTableIndexes = tableSpans
|
||||
.map((span, index) => ({ span, index }))
|
||||
|
|
@ -1173,13 +1137,12 @@ function xmlCandidates(document: DecodedSourceDocument): {
|
|||
const reportScores = xmlReportScores(document);
|
||||
const candidates: FindingCandidate[] = [];
|
||||
const seenRuleIds = new Set<string>();
|
||||
const ruleDiagnostics: ImportDiagnostic[] = [];
|
||||
for (const group of groups) {
|
||||
if (candidates.length >= maximumRuleCandidates) break;
|
||||
const riskIdNode = group.direct.get('riskid');
|
||||
const ruleId = nodeValue(riskIdNode);
|
||||
const identity = ruleId.toLocaleUpperCase('en-US');
|
||||
if (!identity || seenRuleIds.has(identity)) continue;
|
||||
seenRuleIds.add(identity);
|
||||
if (!acceptPingCastleRuleId(ruleId, seenRuleIds, riskIdNode?.source ?? { fileName: document.file.name, objectPath: group.path }, ruleDiagnostics)) continue;
|
||||
const rationaleNode = group.direct.get('rationale');
|
||||
const noticeNode = group.direct.get('notice');
|
||||
const source = riskIdNode?.source
|
||||
|
|
@ -1214,7 +1177,7 @@ function xmlCandidates(document: DecodedSourceDocument): {
|
|||
fields
|
||||
});
|
||||
}
|
||||
const diagnostics: ImportDiagnostic[] = [{
|
||||
const diagnostics: ImportDiagnostic[] = [...ruleDiagnostics, {
|
||||
severity: 'warning',
|
||||
code: 'pingcastle-xml-results-unavailable',
|
||||
message: 'PingCastle-XML wird als eigenständiger Import verarbeitet. Results sind nur aus dem zugehörigen HTML-Bericht verfügbar.',
|
||||
|
|
@ -1228,37 +1191,51 @@ function xmlCandidates(document: DecodedSourceDocument): {
|
|||
stage: 'extraction'
|
||||
});
|
||||
const contextNodes = hierarchyView(document)?.nodes
|
||||
.filter(node => !/\/RiskRules\[\d+\]\//i.test(node.path) && nodeValue(node)) ?? [];
|
||||
.filter(node => !/\/RiskRules\[\d+\](?:\/|$)/i.test(node.path)) ?? [];
|
||||
const parentPaths = new Set(contextNodes.map(node => node.path.slice(0, node.path.lastIndexOf('/'))));
|
||||
const environmentObservations: EnvironmentObservationCandidate[] = [];
|
||||
for (const node of contextNodes) {
|
||||
const value = nodeValue(node);
|
||||
const xmlAttributes = node.attributes.xmlAttributes;
|
||||
const attributeEntries = xmlAttributes && typeof xmlAttributes === 'object' && !Array.isArray(xmlAttributes)
|
||||
? Object.entries(xmlAttributes).filter((entry): entry is [string, string] => typeof entry[1] === 'string') : [];
|
||||
const base = {
|
||||
version: 1 as const, subject: { type: 'pingcastle-xml-node', identity: node.path },
|
||||
property: `pingcastle.xml.${propertyKey(node.path)}`,
|
||||
label: node.name, sectionKey: 'HealthcheckData', source: node.source,
|
||||
attributes: { xmlPath: node.path }
|
||||
};
|
||||
if (value || (!parentPaths.has(node.path) && !attributeEntries.length && !node.attributes.serialized)) {
|
||||
environmentObservations.push({ ...base, originalValue: value || null,
|
||||
...(value ? typedValue(value) : { valueType: 'object' as const }), state: value ? 'observed' : 'empty' });
|
||||
}
|
||||
for (const [name, attributeValue] of attributeEntries) {
|
||||
environmentObservations.push({ ...base, property: `${base.property}.attribute.${propertyKey(name)}`, label: `${node.name} @${name}`,
|
||||
originalValue: attributeValue, ...typedValue(attributeValue), state: attributeValue.length ? 'observed' : 'empty',
|
||||
source: { ...node.source, objectPath: `${node.path}/@${name}` }, attributes: { ...base.attributes, attributeName: name } });
|
||||
}
|
||||
if (!xmlAttributes && typeof node.attributes.serialized === 'string' && node.attributes.serialized) {
|
||||
// Compatibility with older/custom adapters: retain the serialization; never guess its ambiguous quoting.
|
||||
environmentObservations.push({ ...base, property: `${base.property}.serialized.attributes`, label: `${node.name} attributes`,
|
||||
originalValue: node.attributes.serialized, valueType: 'string', state: 'unsupported' });
|
||||
diagnostics.push({ severity: 'warning', code: 'pingcastle-xml-attributes-unstructured', stage: 'extraction', source: node.source,
|
||||
message: 'XML-Attribute liegen nur als Anzeigezeichenkette vor. Der Originalwert bleibt erhalten, eine Attributnormalisierung erfolgt nicht.' });
|
||||
}
|
||||
}
|
||||
const section: ScannerReportSectionCandidate = {
|
||||
version: 1,
|
||||
sectionKey: 'HealthcheckData',
|
||||
label: 'PingCastle HealthcheckData',
|
||||
ordinal: 0,
|
||||
state: contextNodes.length ? 'observed' : 'empty',
|
||||
state: environmentObservations.some(item => item.state === 'observed') ? 'observed' : 'empty',
|
||||
tables: [{
|
||||
name: 'HealthcheckData context',
|
||||
headers: ['Path', 'Value'],
|
||||
rows: contextNodes.map(node => [node.path, nodeValue(node)])
|
||||
rows: environmentObservations.map(observation => [observation.source.objectPath ?? '', observation.originalValue])
|
||||
}],
|
||||
source: { fileName: document.file.name, objectPath: 'HealthcheckData' },
|
||||
attributes: { representation: 'xml-hierarchy' }
|
||||
};
|
||||
const environmentObservations: EnvironmentObservationCandidate[] = contextNodes.map(node => {
|
||||
const value = nodeValue(node);
|
||||
const typed = typedValue(value);
|
||||
return {
|
||||
version: 1,
|
||||
subject: { type: 'pingcastle-xml-node', identity: node.path },
|
||||
property: `pingcastle.xml.${propertyKey(node.path)}`,
|
||||
label: node.path.split('/').at(-1)?.replace(/\[\d+\]$/, '') || node.path,
|
||||
originalValue: value,
|
||||
...typed,
|
||||
state: 'observed',
|
||||
sectionKey: 'HealthcheckData',
|
||||
source: node.source,
|
||||
attributes: { xmlPath: node.path }
|
||||
};
|
||||
});
|
||||
return { candidates, diagnostics, reportSections: [section], environmentObservations };
|
||||
}
|
||||
|
||||
|
|
@ -1270,7 +1247,7 @@ function isHealthcheckXml(document: DecodedSourceDocument): boolean {
|
|||
|
||||
export const pingCastleSourceExtractor: SourceExtractor = {
|
||||
id: 'pingcastle-healthcheck-source-extractor',
|
||||
version: '2.1.0',
|
||||
version: '2.2.1',
|
||||
detect(document) {
|
||||
if (isHealthcheckXml(document)) {
|
||||
const riskRuleCount = xmlRuleGroups(document).length;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,9 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { DecodedSourceDocument, ImportDiagnostic } from '../domain/import-intermediate';
|
||||
import type {
|
||||
FindingCandidate, SourceExtractor, SourceFieldCandidate, SupportingTableCandidate
|
||||
} from '../domain/source-extraction';
|
||||
import type { FindingCandidate, SourceExtractor, SourceFieldCandidate, SupportingTableCandidate } from '../domain/source-extraction';
|
||||
import { cellText, plainTextValue } from '../shared/text';
|
||||
import { embeddedObjectAfter, isJsonObject, readEmbeddedJsonObject } from './embedded-report-json';
|
||||
|
||||
const SCRIPT_GLOBAL = 'win' + 'dow';
|
||||
const CATEGORY_ASSIGNMENT = new RegExp(`${SCRIPT_GLOBAL}\\["Category_(\\d+)"\\]\\["([^"]+)"\\]\\s*=\\s*`, 'g');
|
||||
const REPORT_ASSIGNMENT = new RegExp(`${SCRIPT_GLOBAL}\\.reportJSON\\s*=\\s*`, 'g');
|
||||
const MAX_INDICATORS = 250_000;
|
||||
|
||||
interface PurplePayload {
|
||||
indicators: Array<{ value: Record<string, JsonValue>; path: string }>;
|
||||
categoryNames: Map<number, string>;
|
||||
recognized: boolean;
|
||||
diagnostics: ImportDiagnostic[];
|
||||
}
|
||||
import { isJsonObject } from './embedded-report-json';
|
||||
import { purpleKnightPayload } from './purple-knight-payload';
|
||||
import { purpleKnightReportContext, purpleKnightStatus } from './purple-knight-report-context';
|
||||
|
||||
const EXCEL_HEADERS = [
|
||||
'ShortName', 'Name', 'SI version', 'Target', 'Status', 'Runtime', 'Score', 'Severity', 'Weight', 'Category',
|
||||
|
|
@ -25,56 +12,16 @@ const EXCEL_HEADERS = [
|
|||
'Description', 'Likelihood of compromise', 'Result message', 'Number of results', 'Result', 'Remediation'
|
||||
] as const;
|
||||
|
||||
function purplePayload(document: DecodedSourceDocument): PurplePayload {
|
||||
if (document.format !== 'HTML') return { indicators: [], categoryNames: new Map(), recognized: false, diagnostics: [] };
|
||||
const root = embeddedObjectAfter(document.rawText, REPORT_ASSIGNMENT);
|
||||
if (!root || !isJsonObject(root.value) || !Array.isArray(root.value.reportResultsList)) {
|
||||
return { indicators: [], categoryNames: new Map(), recognized: false, diagnostics: [] };
|
||||
}
|
||||
const categoryNames = new Map<number, string>();
|
||||
const generalConfig = isJsonObject(root.value.GeneralConfig) ? root.value.GeneralConfig : undefined;
|
||||
const categories = generalConfig && Array.isArray(generalConfig.Categories) ? generalConfig.Categories : [];
|
||||
for (const category of categories) {
|
||||
if (!isJsonObject(category) || typeof category.ID !== 'number' || typeof category.Name !== 'string') continue;
|
||||
categoryNames.set(category.ID, category.Name);
|
||||
}
|
||||
const indicators: PurplePayload['indicators'] = [];
|
||||
for (const match of document.rawText.matchAll(CATEGORY_ASSIGNMENT)) {
|
||||
if (indicators.length >= MAX_INDICATORS) break;
|
||||
const start = document.rawText.indexOf('{', match.index! + match[0].length);
|
||||
const embedded = start >= 0 ? readEmbeddedJsonObject(document.rawText, start) : undefined;
|
||||
if (!embedded || !isJsonObject(embedded.value) || !isJsonObject(embedded.value.ResIndicator)) continue;
|
||||
indicators.push({ value: embedded.value, path: `$.Category_${match[1]}[${JSON.stringify(match[2])}]` });
|
||||
}
|
||||
const diagnostics: ImportDiagnostic[] = [];
|
||||
if (indicators.length >= MAX_INDICATORS) diagnostics.push({
|
||||
severity: 'warning', code: 'purple-knight-indicator-limit', stage: 'extraction',
|
||||
message: `Der PurpleKnight-Bericht wurde auf ${MAX_INDICATORS} Indikatoren begrenzt.`
|
||||
});
|
||||
return { indicators, categoryNames, recognized: true, diagnostics };
|
||||
}
|
||||
|
||||
function field(
|
||||
fileName: string, path: string, key: string, label: string, value: JsonValue,
|
||||
decodedValue = cellText(value), columnIndex?: number
|
||||
): SourceFieldCandidate {
|
||||
return {
|
||||
key, label, originalValue: value, decodedValue,
|
||||
source: { fileName, objectPath: `${path}.${key}`, columnIndex }
|
||||
source: { fileName, objectPath: path, columnIndex }
|
||||
};
|
||||
}
|
||||
|
||||
function purpleStatus(indicator: Record<string, JsonValue>, execution: Record<string, JsonValue>): string {
|
||||
if (indicator.IsFailedToRun === true) return 'Failed to run';
|
||||
if (indicator.IsAborted === true) return 'Canceled';
|
||||
if (indicator.Selected === false) return 'Not selected';
|
||||
if (indicator.IsEvaluated === false) return 'Not Relevant';
|
||||
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOE/i.test(indicator.ExposureType)) return 'IOE Found';
|
||||
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOC/i.test(indicator.ExposureType)) return 'IOC Found';
|
||||
const status = execution.Status ?? indicator.State;
|
||||
return typeof status === 'string' ? status.replace(/([a-z])([A-Z])/g, '$1 $2') : '';
|
||||
}
|
||||
|
||||
function target(indicator: Record<string, JsonValue>): string {
|
||||
const targets = Array.isArray(indicator.Targets) ? indicator.Targets.map(cellText).filter(Boolean) : [];
|
||||
return targets.includes('AAD') && targets.includes('AD') ? 'Hybrid (AD)' : targets.join(', ');
|
||||
|
|
@ -113,15 +60,6 @@ function frameworkValues(indicator: Record<string, JsonValue>, frameworkName: st
|
|||
return Array.from({ length: count }, (_, index) => tags[index] ?? null);
|
||||
}
|
||||
|
||||
function workbookResultValue(header: string, value: string): string {
|
||||
if (!/(?:date|timestamp|lastset)$/i.test(header)) return value;
|
||||
const match = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})(?::\d{2})?\s+(AM|PM)$/i);
|
||||
if (!match) return value;
|
||||
let hour = Number(match[4]) % 12;
|
||||
if (match[6].toUpperCase() === 'PM') hour += 12;
|
||||
return `${match[3]}-${match[1].padStart(2, '0')}-${match[2].padStart(2, '0')} ${String(hour).padStart(2, '0')}:${match[5]}`;
|
||||
}
|
||||
|
||||
function reportTable(
|
||||
fileName: string, item: Record<string, JsonValue>, path: string
|
||||
): SupportingTableCandidate | undefined {
|
||||
|
|
@ -140,10 +78,10 @@ function reportTable(
|
|||
name,
|
||||
headers,
|
||||
rows: rows.map(row => headers.map((header, index) => {
|
||||
const value = cellText(row[index]);
|
||||
const value = typeof row[index] === 'string' ? row[index] as string : cellText(row[index]);
|
||||
const prefix = `${header}:`;
|
||||
const unprefixed = value.startsWith(prefix) ? value.slice(prefix.length).trimStart() : value;
|
||||
return workbookResultValue(header, unprefixed);
|
||||
const unprefixed = value.startsWith(prefix) ? value.slice(prefix.length).replace(/^ /, '') : value;
|
||||
return unprefixed;
|
||||
})),
|
||||
source: { fileName, objectPath: `${path}.IndicatorReportObjects` },
|
||||
attributes: { kind: 'finding-result', findingReference: name }
|
||||
|
|
@ -166,45 +104,86 @@ function indicatorCandidate(
|
|||
{ key: 'Name', value: indicator.Name ?? null },
|
||||
{ key: 'Version', value: indicator.Version ?? null },
|
||||
{ key: 'Targets', value: indicator.Targets ?? null, decoded: target(indicator) },
|
||||
{ key: 'Status', value: execution.Status ?? indicator.State ?? null, decoded: purpleStatus(indicator, execution) },
|
||||
{ key: 'Runtime', value: runtime(indicator) },
|
||||
{ key: 'Status', value: item, decoded: purpleKnightStatus(indicator, execution) },
|
||||
{ key: 'Runtime', value: indicator, decoded: runtime(indicator) },
|
||||
{ key: 'Score', value: execution.Score ?? null },
|
||||
{ key: 'Severity', value: indicator.Severity ?? null },
|
||||
{ key: 'Weight', value: indicator.Weight ?? null },
|
||||
{ key: 'CategoryID', value: indicator.CategoryID ?? null, decoded: category(indicator, categoryNames) },
|
||||
{ key: 'CategoryID', value: indicator, decoded: category(indicator, categoryNames) },
|
||||
...frameworkTags.map((value, index) => ({ key: `Framework_${index + 1}`, value })),
|
||||
{ key: 'Description', value: indicator.Description ?? null, decoded: plainTextValue(indicator.Description) },
|
||||
{ key: 'LikelihoodOfCompromise', value: indicator.LikelihoodOfCompromise ?? null,
|
||||
decoded: plainTextValue(indicator.LikelihoodOfCompromise) },
|
||||
{ key: 'ResultMessage', value: execution.ResultMessage ?? null },
|
||||
{ key: 'TotalResultsCount', value: item.TotalResultsCount ?? 0 },
|
||||
{ key: 'Result', value: hasResults ? indicator.ShortName ?? null : null },
|
||||
{ key: 'TotalResultsCount', value: item.TotalResultsCount ?? null },
|
||||
{ key: 'Result', value: item.IndicatorReportObjects ?? null, decoded: hasResults ? cellText(indicator.ShortName) : '' },
|
||||
{ key: 'Remediation', value: execution.Remediation ?? null, decoded: plainTextValue(execution.Remediation) }
|
||||
];
|
||||
const fields = values.map((itemValue, columnIndex) => field(
|
||||
fileName, path, itemValue.key, EXCEL_HEADERS[columnIndex], itemValue.value,
|
||||
itemValue.decoded ?? cellText(itemValue.value), columnIndex
|
||||
));
|
||||
const directPath: Record<string, string> = {
|
||||
Status: path, Runtime: `${path}.ResIndicator`, CategoryID: `${path}.ResIndicator`,
|
||||
Score: `${path}.ExecutionResult.Score`, ResultMessage: `${path}.ExecutionResult.ResultMessage`,
|
||||
Remediation: `${path}.ExecutionResult.Remediation`, TotalResultsCount: `${path}.TotalResultsCount`,
|
||||
Result: `${path}.IndicatorReportObjects`
|
||||
};
|
||||
const frameworkPaths = ['MITRE ATT&CK', 'ANSSI', 'MITRE D3FEND'].flatMap((name, group) => {
|
||||
const all = Array.isArray(indicator.SecurityFrameworks) ? indicator.SecurityFrameworks : [];
|
||||
const paths = all.flatMap((entry, frameworkIndex) => isJsonObject(entry) && entry.Name === name && Array.isArray(entry.Tags)
|
||||
? entry.Tags.map((_, tagIndex) => `${path}.ResIndicator.SecurityFrameworks[${frameworkIndex}].Tags[${tagIndex}]`) : []);
|
||||
return Array.from({ length: group === 2 ? 2 : 4 }, (_, index) => paths[index]);
|
||||
});
|
||||
const fields = values.map((value, columnIndex) => {
|
||||
const framework = /^Framework_(\d+)$/.exec(value.key);
|
||||
const objectPath = framework ? frameworkPaths[Number(framework[1]) - 1] : directPath[value.key] ?? `${path}.ResIndicator.${value.key}`;
|
||||
const result = field(fileName, objectPath ?? path, value.key, EXCEL_HEADERS[columnIndex], value.value,
|
||||
value.decoded ?? cellText(value.value), columnIndex);
|
||||
// Absent compatibility columns have no fictitious native location. An
|
||||
// explicitly null native field still retains its exact source path.
|
||||
if (value.value === null) {
|
||||
const present = framework ? Boolean(frameworkPaths[Number(framework[1]) - 1])
|
||||
: value.key === 'Result' ? Object.hasOwn(item, 'IndicatorReportObjects')
|
||||
: value.key === 'TotalResultsCount' ? Object.hasOwn(item, value.key)
|
||||
: ['Score', 'ResultMessage', 'Remediation'].includes(value.key) ? Object.hasOwn(execution, value.key)
|
||||
: Object.hasOwn(indicator, value.key);
|
||||
if (!present) result.source.objectPath = undefined;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
// Preserve fields beyond the compatibility column set, including complete
|
||||
// framework arrays and future properties. Namespaced labels avoid remapping.
|
||||
const retained = new Set(fields.map(value => value.source.objectPath));
|
||||
for (const [parent, object] of [['ResIndicator', indicator], ['ExecutionResult', execution]] as const) {
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
const objectPath = `${path}.${parent}[${JSON.stringify(key)}]`;
|
||||
if (retained.has(`${path}.${parent}.${key}`)) continue;
|
||||
fields.push(field(fileName, objectPath, `source-${parent}-${key}`, `Source ${parent}.${key}`, value));
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(item)) {
|
||||
if (['ResIndicator', 'ExecutionResult', 'IndicatorReportObjects', 'TotalResultsCount'].includes(key)) continue;
|
||||
fields.push(field(fileName, `${path}[${JSON.stringify(key)}]`, `source-${key}`, `Source ${key}`, value));
|
||||
}
|
||||
return { version: 1, id: `purple-knight-indicator-${index}`, source: { fileName, objectPath: path }, fields };
|
||||
}
|
||||
|
||||
export const purpleKnightHtmlSourceExtractor: SourceExtractor = {
|
||||
id: 'purple-knight-html-source-extractor',
|
||||
version: '1.1.0',
|
||||
version: '1.2.0',
|
||||
detect(document) {
|
||||
const payload = purplePayload(document);
|
||||
return payload.recognized && payload.indicators.length > 0
|
||||
? { score: 1, evidence: [`Sicher eingebettetes PurpleKnight-Reportmodell mit ${payload.indicators.length} Indikatoren erkannt.`] }
|
||||
const payload = purpleKnightPayload(document);
|
||||
return payload.recognized
|
||||
? { score: 1, toolId: 'purple-knight', evidence: [`Sicher eingebettetes PurpleKnight-Reportmodell mit ${payload.indicators.length} Indikatoren erkannt.`] }
|
||||
: { score: 0, evidence: [] };
|
||||
},
|
||||
extract(document) {
|
||||
const payload = purplePayload(document);
|
||||
const payload = purpleKnightPayload(document);
|
||||
const candidates = payload.indicators.map((item, index) =>
|
||||
indicatorCandidate(document.file.name, item.value, item.path, payload.categoryNames, index));
|
||||
const supportingTables = payload.indicators.flatMap(item => {
|
||||
const table = reportTable(document.file.name, item.value, item.path);
|
||||
return table ? [table] : [];
|
||||
});
|
||||
return { version: 1, candidates, supportingTables, diagnostics: payload.diagnostics };
|
||||
const context = payload.root ? purpleKnightReportContext(document.file.name, payload.root, payload.indicators)
|
||||
: { reportSections: [], environmentObservations: [], diagnostics: [] };
|
||||
return { version: 1, candidates, supportingTables, ...context, diagnostics: [...payload.diagnostics, ...context.diagnostics] };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { DecodedSourceDocument, ImportDiagnostic } from '../domain/import-intermediate';
|
||||
import { ImportError } from '../shared/errors';
|
||||
import { isJsonObject } from './embedded-report-json';
|
||||
import { assignedReportObjects, readStaticReportObject, StaticReportLiteralError, type StaticReportBindingMatcher } from './static-report-literal';
|
||||
import type { PurpleKnightIndicator } from './purple-knight-report-context';
|
||||
import { protectHtmlAttributeMarkup } from './html-attribute-markup';
|
||||
|
||||
const MAX_INDICATORS = 250_000;
|
||||
// Inert source syntax, never a dependency on the browser's global object.
|
||||
const SCRIPT_GLOBAL = 'win' + 'dow';
|
||||
const ASSIGNMENT = new RegExp(`^${SCRIPT_GLOBAL}\\s*(?:\\.\\s*(reportJSON)|\\[\\s*["'](Category_\\d+)["']\\s*\\]\\s*\\[\\s*["']([\\w-]+)["']\\s*\\])\\s*=\\s*(?!=|>)`);
|
||||
const reportBinding: StaticReportBindingMatcher = (script, position) => {
|
||||
if (!script.startsWith(SCRIPT_GLOBAL, position)) return undefined;
|
||||
let before = position - 1;
|
||||
while (before >= 0 && /\s/.test(script[before])) before--;
|
||||
if (script[before] === '.') return undefined;
|
||||
const match = ASSIGNMENT.exec(script.slice(position));
|
||||
return match ? { start: position + match[0].length, binding: match[1] ?? `${match[2]}[${JSON.stringify(match[3])}]` } : undefined;
|
||||
};
|
||||
interface PurplePayload {
|
||||
root?: Record<string, JsonValue>;
|
||||
indicators: PurpleKnightIndicator[];
|
||||
categoryNames: Map<number, string>;
|
||||
recognized: boolean;
|
||||
diagnostics: ImportDiagnostic[];
|
||||
}
|
||||
const cache = new WeakMap<DecodedSourceDocument, PurplePayload>();
|
||||
|
||||
function invalid(code: string, message: string): never {
|
||||
throw new ImportError(message, [{ severity: 'error', stage: 'extraction', code, message }]);
|
||||
}
|
||||
|
||||
export function purpleKnightPayload(document: DecodedSourceDocument): PurplePayload {
|
||||
const cached = cache.get(document);
|
||||
if (cached) return cached;
|
||||
const result: PurplePayload = { indicators: [], categoryNames: new Map(), recognized: false, diagnostics: [] };
|
||||
if (document.format !== 'HTML') return result;
|
||||
const bindings = new Set<string>();
|
||||
const names = new Set<string>();
|
||||
try {
|
||||
// Respect quoted attributes, comments and other raw-text elements without
|
||||
// altering the inert script bodies or their native JSON string values.
|
||||
const markup = protectHtmlAttributeMarkup(document.rawText);
|
||||
const scripts = /<!--[\s\S]*?-->|<(script|style|textarea|title|xmp|iframe|noembed|noframes)\b[^>]*>([\s\S]*?)(?:<\/\1\s*>|$)/gi;
|
||||
for (const script of markup.matchAll(scripts)) {
|
||||
if (script[1]?.toLowerCase() !== 'script') continue;
|
||||
for (const { start, binding } of assignedReportObjects(script[2], reportBinding)) {
|
||||
if (binding !== 'reportJSON' && !binding.startsWith('Category_')) continue;
|
||||
result.recognized = true;
|
||||
if (bindings.has(binding)) invalid('purple-knight-binding-duplicate', 'PurpleKnight contains conflicting report assignments.');
|
||||
bindings.add(binding);
|
||||
const { value } = readStaticReportObject(script[2], start);
|
||||
if (binding === 'reportJSON') {
|
||||
if (!Array.isArray(value.reportResultsList) || value.reportResultsList.some(scope => !isJsonObject(scope) || !isJsonObject(scope.ReportResults))) invalid('purple-knight-payload-invalid', 'PurpleKnight report summary is malformed.');
|
||||
result.root = value;
|
||||
} else {
|
||||
if (result.indicators.length >= MAX_INDICATORS) invalid('purple-knight-indicator-limit', 'PurpleKnight indicator limit exceeded; no results were silently truncated.');
|
||||
if (!isJsonObject(value.ResIndicator) || !isJsonObject(value.ExecutionResult)) invalid('purple-knight-indicator-invalid', 'A PurpleKnight indicator is malformed.');
|
||||
const name = value.ResIndicator.ShortName;
|
||||
if (typeof name !== 'string' || !name.trim()) invalid('purple-knight-id-missing', 'A PurpleKnight indicator has no native rule ID.');
|
||||
if (names.has(name)) result.diagnostics.push({ severity: 'warning', code: 'purple-knight-id-duplicate', stage: 'extraction', message: 'PurpleKnight contains repeated native rule IDs. All occurrences were retained.' });
|
||||
names.add(name);
|
||||
if (value.IndicatorReportObjects != null && (!Array.isArray(value.IndicatorReportObjects) || value.IndicatorReportObjects.some(row => !Array.isArray(row)))) invalid('purple-knight-detail-invalid', 'A PurpleKnight result table is malformed.');
|
||||
const identity = JSON.parse(binding.slice(binding.indexOf('[') + 1, -1)) as string;
|
||||
result.indicators.push({ value, path: `$.${binding}`, identity });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (!(error instanceof StaticReportLiteralError)) throw error;
|
||||
if (result.recognized) invalid('purple-knight-payload-invalid', 'PurpleKnight data could not be read completely. Only bounded static report objects are supported.');
|
||||
}
|
||||
if (result.recognized && !result.root) invalid('purple-knight-summary-missing', 'PurpleKnight indicator data has no readable report summary.');
|
||||
if (result.root) {
|
||||
const scopes = result.root.reportResultsList as JsonValue[];
|
||||
if (!result.indicators.length && (!scopes.length || scopes.some(scope => !isJsonObject(scope) || !isJsonObject(scope.ReportResults) || scope.ReportResults.TotalCount !== 0))) invalid('purple-knight-results-missing', 'PurpleKnight announces results but no indicators could be read.');
|
||||
const config = isJsonObject(result.root.GeneralConfig) ? result.root.GeneralConfig : {};
|
||||
for (const category of Array.isArray(config.Categories) ? config.Categories : []) if (isJsonObject(category) && typeof category.ID === 'number' && typeof category.Name === 'string') result.categoryNames.set(category.ID, category.Name);
|
||||
}
|
||||
cache.set(document, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { ImportDiagnostic } from '../domain/import-intermediate';
|
||||
import type { EnvironmentObservationCandidate, ScannerReportSectionCandidate } from '../domain/source-extraction';
|
||||
import { isJsonObject } from './embedded-report-json';
|
||||
import { purpleKnightMetricObservation } from './purple-knight-summary-source-extractor';
|
||||
|
||||
export interface PurpleKnightIndicator {
|
||||
value: Record<string, JsonValue>;
|
||||
path: string;
|
||||
identity: string;
|
||||
}
|
||||
|
||||
export function purpleKnightStatus(indicator: Record<string, JsonValue>, execution: Record<string, JsonValue>): string {
|
||||
if (indicator.IsFailedToRun === true) return 'Failed to run';
|
||||
if (indicator.IsAborted === true) return 'Canceled';
|
||||
if (indicator.Selected === false) return 'Not selected';
|
||||
if (indicator.IsEvaluated === false) return 'Not Relevant';
|
||||
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOE/i.test(indicator.ExposureType)) return 'IOE Found';
|
||||
if (indicator.IsFailed === true && typeof indicator.ExposureType === 'string' && /IOC/i.test(indicator.ExposureType)) return 'IOC Found';
|
||||
const status = execution.Status ?? indicator.State;
|
||||
return typeof status === 'string' ? status.replace(/([a-z])([A-Z])/g, '$1 $2') : '';
|
||||
}
|
||||
|
||||
const METRICS: Record<string, string> = {
|
||||
TotalScore: 'Security posture score', TotalCount: 'Total indicators', TotalEvaluatedCount: 'Evaluated',
|
||||
PassedCount: 'Passed', IOEsCount: 'IOEs found', IOCsCount: 'IOCs found', FailedToRunCount: 'Failed to run',
|
||||
AbortedCount: 'Canceled', UnselectedCount: 'Not selected', NotRelevant: 'Not relevant',
|
||||
TestExecutionTime: 'Scan duration', TotalSelectedCount: 'Selected indicators'
|
||||
};
|
||||
|
||||
function countDiagnostic(value: JsonValue, actual: number, path: string): ImportDiagnostic[] {
|
||||
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) return [{
|
||||
severity: 'warning', code: 'purple-knight-count-invalid', stage: 'extraction',
|
||||
message: 'A declared PurpleKnight count is invalid.', details: { objectPath: path }
|
||||
}];
|
||||
return value === actual ? [] : [{
|
||||
severity: 'warning', code: 'purple-knight-count-mismatch', stage: 'extraction',
|
||||
message: `PurpleKnight declares ${value} results; ${actual} were independently counted.`,
|
||||
details: { objectPath: path, declared: value, actual }
|
||||
}];
|
||||
}
|
||||
|
||||
export function purpleKnightReportContext(fileName: string, root: Record<string, JsonValue>, indicators: PurpleKnightIndicator[]) {
|
||||
const sectionKey = 'purple-knight-assessment-summary';
|
||||
const rootPath = '$.reportJSON';
|
||||
// Keep all report metadata, including future nested keys, separate from findings.
|
||||
const reportSections: ScannerReportSectionCandidate[] = [{
|
||||
version: 1, sectionKey, label: 'PurpleKnight assessment summary', ordinal: 0, state: 'observed',
|
||||
tables: [{ name: 'Report metadata', headers: ['Property', 'Original value'], rows: Object.entries(root) }],
|
||||
source: { fileName, objectPath: rootPath }, attributes: { kind: 'purple-knight-report-context' }
|
||||
}];
|
||||
const environmentObservations: EnvironmentObservationCandidate[] = Object.entries(root).map(([key, value]) => ({
|
||||
version: 1, sectionKey, subject: { type: 'purple-knight-report', identity: rootPath },
|
||||
property: `purple_knight.source.${key}`, label: key, originalValue: value, normalizedValue: value,
|
||||
valueType: value !== null && typeof value === 'object' ? 'object' : typeof value === 'number' ? 'number' : typeof value === 'boolean' ? 'boolean' : 'string',
|
||||
state: value === null || value === '' ? 'empty' : 'observed',
|
||||
source: { fileName, objectPath: `${rootPath}.${key}` }, attributes: { sourceField: key }
|
||||
}));
|
||||
const diagnostics: ImportDiagnostic[] = [];
|
||||
const metric = (key: string, value: JsonValue | undefined, path: string) => {
|
||||
if (value !== undefined) environmentObservations.push(purpleKnightMetricObservation(key, value, { fileName, objectPath: path }));
|
||||
};
|
||||
metric('Tool version', root.AppVersion, `${rootPath}.AppVersion`);
|
||||
metric('Tool edition', root.Edition, `${rootPath}.Edition`);
|
||||
const scopes = Array.isArray(root.reportResultsList) ? root.reportResultsList : [];
|
||||
const byIdentity = new Map(indicators.map(item => [item.identity, item]));
|
||||
for (const [index, scope] of scopes.entries()) {
|
||||
if (!isJsonObject(scope) || !isJsonObject(scope.ReportResults)) continue;
|
||||
const path = `${rootPath}.reportResultsList[${index}]`;
|
||||
const prefix = scope.ADType === 'AD' ? 'AD' : /^(?:AAD|Entra)$/i.test(String(scope.ADType)) ? 'Entra' : 'Other';
|
||||
const results = scope.ReportResults;
|
||||
metric('Generated date', results.GeneratedOn, `${path}.ReportResults.GeneratedOn`);
|
||||
for (const [key, label] of Object.entries(METRICS)) metric(key === 'TotalSelectedCount' && scopes.length === 1 ? 'Selected indicators' : `${prefix}: ${label}`, results[key], `${path}.ReportResults.${key}`);
|
||||
metric(`${prefix}: Forest name`, scope.ForestName, `${path}.ForestName`);
|
||||
if (Array.isArray(scope.SelectedDomains)) {
|
||||
environmentObservations.push({
|
||||
...purpleKnightMetricObservation(`${prefix}: Number of domains`, scope.SelectedDomains, { fileName, objectPath: `${path}.SelectedDomains` }),
|
||||
normalizedValue: scope.SelectedDomains.length, valueType: 'number', attributes: { derived: true, derivation: 'array-length' }
|
||||
});
|
||||
}
|
||||
metric(`${prefix}: Domains`, scope.SelectedDomains, `${path}.SelectedDomains`);
|
||||
metric('Requested by', scope.UserName, `${path}.UserName`);
|
||||
const referenced = new Set<string>();
|
||||
for (const [categoryIndex, entry] of (Array.isArray(scope.ReportCategories) ? scope.ReportCategories : []).entries()) {
|
||||
if (!isJsonObject(entry)) continue;
|
||||
const category = isJsonObject(entry.Category) ? entry.Category : {};
|
||||
if (typeof category.Name === 'string') metric(`${prefix} category score: ${category.Name}`, category.Score, `${path}.ReportCategories[${categoryIndex}].Category.Score`);
|
||||
if (Array.isArray(entry.Indicators)) for (const identity of entry.Indicators) if (typeof identity === 'string') referenced.add(identity);
|
||||
}
|
||||
const scoped = scopes.length === 1 ? indicators : [...referenced].flatMap(id => byIdentity.has(id) ? [byIdentity.get(id)!] : []);
|
||||
const counts: Record<string, number> = { TotalCount: scoped.length, PassedCount: 0, IOEsCount: 0, IOCsCount: 0, FailedToRunCount: 0, AbortedCount: 0, UnselectedCount: 0, NotRelevant: 0, TotalEvaluatedCount: 0 };
|
||||
const statusKeys: Record<string, string> = { Pass: 'PassedCount', Passed: 'PassedCount', 'IOE Found': 'IOEsCount', 'IOC Found': 'IOCsCount', 'Failed to run': 'FailedToRunCount', Canceled: 'AbortedCount', 'Not selected': 'UnselectedCount', 'Not Relevant': 'NotRelevant' };
|
||||
for (const item of scoped) {
|
||||
const indicator = isJsonObject(item.value.ResIndicator) ? item.value.ResIndicator : {};
|
||||
const status = purpleKnightStatus(indicator, isJsonObject(item.value.ExecutionResult) ? item.value.ExecutionResult : {});
|
||||
if (Object.hasOwn(statusKeys, status)) counts[statusKeys[status]]++;
|
||||
if (indicator.IsEvaluated === true) counts.TotalEvaluatedCount++;
|
||||
}
|
||||
for (const [key, actual] of Object.entries(counts)) if (key in results) diagnostics.push(...countDiagnostic(results[key], actual, `${path}.ReportResults.${key}`));
|
||||
for (const id of referenced) if (!byIdentity.has(id)) diagnostics.push({ severity: 'warning', code: 'purple-knight-reference-missing', stage: 'extraction', message: 'A report category references a missing indicator.', details: { objectPath: `${path}.ReportCategories` } });
|
||||
}
|
||||
for (const item of indicators) {
|
||||
const rows = Array.isArray(item.value.IndicatorReportObjects) ? item.value.IndicatorReportObjects : [];
|
||||
if ('TotalResultsCount' in item.value) diagnostics.push(...countDiagnostic(item.value.TotalResultsCount, rows.length, `${item.path}.TotalResultsCount`));
|
||||
if (isJsonObject(root.IndicatorsTotalCount) && item.identity in root.IndicatorsTotalCount) diagnostics.push(...countDiagnostic(root.IndicatorsTotalCount[item.identity], rows.length, `${rootPath}.IndicatorsTotalCount[${JSON.stringify(item.identity)}]`));
|
||||
}
|
||||
return { reportSections, environmentObservations, diagnostics };
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* Umgebungskontext aus der Purple-Knight-Zusammenfassung.
|
||||
*
|
||||
* Purple Knight exportiert neben den Indikatorergebnissen eine eigene
|
||||
* Zusammenfassungsdatei aus zwei Spalten: links die Kennzahl, rechts ihr Wert.
|
||||
* Dort stehen genau die Angaben, die kein einzelner Befund traegt - der
|
||||
* Posture Score, die Kategoriewerte, der Forestname, die Zahl der Domaenen und
|
||||
* die Auswertungsbilanz (evaluated, IOEs found, passed, not relevant).
|
||||
*
|
||||
* Ohne diesen Extractor lief die Datei in die allgemeine Tabellenextraktion:
|
||||
* sie erzeugte dort keine Befunde (die Zeilen sind keine Indikatoren) und ihr
|
||||
* Inhalt blieb ausschliesslich als Quelltabelle liegen. Die Kennzahlen waren
|
||||
* damit importiert, aber fuer Auswertung und Darstellung unerreichbar.
|
||||
*
|
||||
* Summary-Zeilen liefern ausschliesslich Kontext. Andere Tabellen derselben
|
||||
* Datei durchlaufen weiterhin den generischen Extractor und das bestaetigte
|
||||
* Tabellenmapping. Ein Summary darf niemals Workbook-Findings verdraengen.
|
||||
*/
|
||||
|
||||
import type { JsonValue } from '../domain/canonical-model';
|
||||
import type { EnvironmentSubject, EnvironmentValueType } from '../domain/environment-context';
|
||||
import type { DecodedSourceDocument, ImportTable, SourceLocator } from '../domain/import-intermediate';
|
||||
import type { EnvironmentObservationCandidate, SourceExtractor } from '../domain/source-extraction';
|
||||
import { clean, normalizeNumericValue } from '../shared/text';
|
||||
import { genericTableSourceExtractor } from './generic-table-source-extractor';
|
||||
|
||||
/** Abschnitt "AD category score: <Kategorie>" der Zusammenfassung. */
|
||||
const CATEGORY_SCORE = /^(ad|aad|entra)\s+category\s+score:\s*(.+)$/i;
|
||||
/** Kennzahl mit vorangestelltem Erhebungsbereich, z. B. "AD: Passed". */
|
||||
const SCOPED_METRIC = /^(ad|aad|entra(?:\s*id)?):\s*(.+)$/i;
|
||||
/** Kopfzeile der Zusammenfassung: "Tool edition" und die eingesetzte Edition. */
|
||||
const TOOL_EDITION = /^tool\s+edition$/i;
|
||||
|
||||
/**
|
||||
* Kennzahlen, die diese Datei zweifelsfrei als Purple-Knight-Zusammenfassung
|
||||
* ausweisen. Zwei Treffer sind gefordert, damit eine beliebige zweispaltige
|
||||
* Tabelle mit einem aehnlich benannten Feld nicht faelschlich hier landet.
|
||||
*/
|
||||
const SUMMARY_KEYS = [
|
||||
/^ad:\s*security\s+posture\s+score$/i,
|
||||
/^ad:\s*ioes?\s+found$/i,
|
||||
/^ad:\s*number\s+of\s+domains$/i,
|
||||
/^ad:\s*forest\s+name$/i,
|
||||
/^ad\s+category\s+score:/i,
|
||||
/^selected\s+indicators$/i
|
||||
];
|
||||
|
||||
const ENVIRONMENT: EnvironmentSubject = { type: 'active-directory-environment' };
|
||||
|
||||
interface SummaryPair {
|
||||
key: string;
|
||||
value: JsonValue;
|
||||
tableIndex: number;
|
||||
rowIndex: number;
|
||||
columnIndex: number;
|
||||
}
|
||||
|
||||
interface MetricMapping {
|
||||
property: string;
|
||||
label: string;
|
||||
valueType: EnvironmentValueType;
|
||||
subject: EnvironmentSubject;
|
||||
}
|
||||
|
||||
function assessmentSubject(fileName: string): EnvironmentSubject {
|
||||
return { type: 'purple-knight-assessment', identity: fileName, label: 'Purple Knight assessment' };
|
||||
}
|
||||
|
||||
function propertyKey(value: string): string {
|
||||
return value.toLocaleLowerCase('en-US').replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'value';
|
||||
}
|
||||
|
||||
function scopeKey(scope: string): string {
|
||||
return /^ad$/i.test(scope) ? 'ad' : 'entra';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kennzahlen, deren Aussage nicht werkzeugspezifisch ist, werden auf die
|
||||
* gemeinsamen Eigenschaftsnamen abgebildet. Alles andere bleibt bewusst unter
|
||||
* `purple_knight.*`, damit eine native Toolzahl nie unbeschriftet in einer
|
||||
* werkzeuguebergreifenden Kennzahl landet.
|
||||
*/
|
||||
const SHARED_PROPERTIES: Array<[RegExp, MetricMapping]> = [
|
||||
[/^generated\s+date$/i, {
|
||||
property: 'report.generated_at', label: 'Report generation date', valueType: 'datetime', subject: ENVIRONMENT
|
||||
}],
|
||||
[/^ad:\s*forest\s+name$/i, {
|
||||
property: 'forest.name', label: 'Forest name', valueType: 'string', subject: ENVIRONMENT
|
||||
}],
|
||||
[/^ad:\s*number\s+of\s+domains$/i, {
|
||||
property: 'domain.count', label: 'Number of domains', valueType: 'number', subject: ENVIRONMENT
|
||||
}]
|
||||
];
|
||||
|
||||
function mappingFor(key: string, fileName: string): MetricMapping {
|
||||
const shared = SHARED_PROPERTIES.find(([pattern]) => pattern.test(key));
|
||||
if (shared) return shared[1];
|
||||
const category = CATEGORY_SCORE.exec(key);
|
||||
if (category) {
|
||||
const name = clean(category[2]);
|
||||
return {
|
||||
property: `purple_knight.${scopeKey(category[1])}.category_score.${propertyKey(name)}`,
|
||||
label: name,
|
||||
valueType: 'number',
|
||||
subject: { type: 'security-domain', identity: propertyKey(name), label: name }
|
||||
};
|
||||
}
|
||||
const scoped = SCOPED_METRIC.exec(key);
|
||||
if (scoped) {
|
||||
return {
|
||||
property: `purple_knight.${scopeKey(scoped[1])}.${propertyKey(scoped[2])}`,
|
||||
label: clean(scoped[2]),
|
||||
valueType: 'string',
|
||||
subject: assessmentSubject(fileName)
|
||||
};
|
||||
}
|
||||
return {
|
||||
property: `purple_knight.${propertyKey(key)}`,
|
||||
label: clean(key),
|
||||
valueType: 'string',
|
||||
subject: assessmentSubject(fileName)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Zahlen werden als Zahl normalisiert, damit Auswertung und Darstellung nicht
|
||||
* erneut parsen muessen. Datumsangaben bleiben Text: die Zusammenfassung nennt
|
||||
* sie ohne Zeitzone, eine stille Umrechnung waere eine Behauptung.
|
||||
*/
|
||||
export function purpleKnightMetricObservation(key: string, value: JsonValue, source: SourceLocator): EnvironmentObservationCandidate {
|
||||
const mapping = mappingFor(key, source.fileName);
|
||||
const numeric = mapping.valueType === 'datetime' ? null : normalizeNumericValue(value);
|
||||
const typed: { normalizedValue: JsonValue; valueType: EnvironmentValueType } = numeric !== null
|
||||
? { normalizedValue: numeric, valueType: 'number' }
|
||||
: { normalizedValue: value, valueType: value !== null && typeof value === 'object' ? 'object' : mapping.valueType };
|
||||
return {
|
||||
version: 1,
|
||||
subject: mapping.subject,
|
||||
property: mapping.property,
|
||||
label: mapping.label,
|
||||
originalValue: value,
|
||||
...typed,
|
||||
state: value === null || value === '' ? 'empty' : 'observed',
|
||||
sectionKey: 'purple-knight-assessment-summary',
|
||||
source,
|
||||
attributes: { sourceMetric: key }
|
||||
};
|
||||
}
|
||||
|
||||
function observation(pair: SummaryPair, fileName: string): EnvironmentObservationCandidate {
|
||||
return purpleKnightMetricObservation(pair.key, pair.value, { fileName, tableIndex: pair.tableIndex, rowIndex: pair.rowIndex, columnIndex: pair.columnIndex });
|
||||
}
|
||||
|
||||
/** Zweispaltige Kennzahlenpaare einer Tabelle, Kopfzeile eingeschlossen. */
|
||||
function summaryPairs(table: ImportTable): SummaryPair[] {
|
||||
return table.rows.flatMap(row => {
|
||||
const key = clean(row.cells[0]?.decodedValue);
|
||||
if (!key) return [];
|
||||
return [{
|
||||
key,
|
||||
value: row.cells[1]?.rawValue ?? null,
|
||||
tableIndex: table.index,
|
||||
rowIndex: row.index,
|
||||
columnIndex: 1
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function summaryTable(document: DecodedSourceDocument): ImportTable | undefined {
|
||||
if (document.format === 'HTML') return undefined;
|
||||
return document.tables.find(table => {
|
||||
const keys = summaryPairs(table).map(pair => pair.key);
|
||||
return SUMMARY_KEYS.filter(pattern => keys.some(key => pattern.test(key))).length >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
export const purpleKnightSummarySourceExtractor: SourceExtractor = {
|
||||
id: 'purple-knight-summary-source-extractor',
|
||||
version: '1.1.0',
|
||||
detect(document) {
|
||||
return summaryTable(document)
|
||||
? { score: 1, toolId: 'purple-knight', evidence: ['PurpleKnight summary metrics recognized.'] }
|
||||
: { score: 0, evidence: [] };
|
||||
},
|
||||
extract(document) {
|
||||
const table = summaryTable(document);
|
||||
if (!table) return { version: 1, candidates: [], diagnostics: [] };
|
||||
const pairs = summaryPairs(table);
|
||||
const editionRow = pairs.find(pair => TOOL_EDITION.test(pair.key));
|
||||
const metrics = pairs.filter(pair => pair !== editionRow);
|
||||
const environmentObservations = [
|
||||
...(editionRow ? [observation(editionRow, document.file.name)] : []),
|
||||
...metrics.map(pair => observation(pair, document.file.name))
|
||||
];
|
||||
return {
|
||||
version: 1,
|
||||
// A context table must never consume the finding tables in the same file.
|
||||
// Keep their generic fields/locators; table selection remains the mapping layer's responsibility.
|
||||
candidates: genericTableSourceExtractor.extract({ ...document, tables: document.tables.filter(item => item.index !== table.index) }).candidates,
|
||||
reportSections: [{
|
||||
version: 1,
|
||||
sectionKey: 'purple-knight-assessment-summary',
|
||||
label: 'Purple Knight assessment summary',
|
||||
ordinal: 0,
|
||||
state: metrics.length ? 'observed' : 'empty',
|
||||
tables: [{
|
||||
name: 'Assessment summary',
|
||||
sourceTableIndex: table.index,
|
||||
headers: ['Metric', 'Value'],
|
||||
rows: metrics.map(pair => [pair.key, pair.value])
|
||||
}],
|
||||
source: { fileName: document.file.name, tableIndex: table.index },
|
||||
attributes: { metricCount: metrics.length }
|
||||
}],
|
||||
environmentObservations,
|
||||
diagnostics: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -27,6 +27,8 @@ export interface DecodedHtmlDocument {
|
|||
export interface DecodedXmlRow {
|
||||
path: string;
|
||||
attributes: string;
|
||||
/** Exact decoded attribute values; the display serialization is not parseable losslessly. */
|
||||
attributeValues?: Record<string, string>;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { defenderForIotSourceExtractor } from './defender-for-iot-source-extract
|
|||
import { pingCastleSourceExtractor } from './pingcastle-source-extractor';
|
||||
import { maesterHtmlSourceExtractor } from './maester-html-source-extractor';
|
||||
import { purpleKnightHtmlSourceExtractor } from './purple-knight-html-source-extractor';
|
||||
import { purpleKnightSummarySourceExtractor } from './purple-knight-summary-source-extractor';
|
||||
import { genericTableSourceExtractor } from './generic-table-source-extractor';
|
||||
export { genericTableSourceExtractor } from './generic-table-source-extractor';
|
||||
|
||||
function safeId(value: string): string {
|
||||
return value.replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||
|
|
@ -59,43 +62,13 @@ export const nativeObjectSourceExtractor: SourceExtractor = {
|
|||
}
|
||||
};
|
||||
|
||||
export const genericTableSourceExtractor: SourceExtractor = {
|
||||
id: 'generic-table-source-extractor',
|
||||
version: '1.0.0',
|
||||
fallback: true,
|
||||
detect(document) {
|
||||
const count = document.tables.length;
|
||||
return count
|
||||
? { score: 0.1, evidence: [`${count} kompatible Tabellenansicht(en) vorhanden.`] }
|
||||
: { score: 0, evidence: [] };
|
||||
},
|
||||
extract(document) {
|
||||
const candidates = document.tables.flatMap(table => {
|
||||
const header = table.headerCandidates[0];
|
||||
return table.rows
|
||||
.filter(row => row.index !== header?.rowIndex && row.cells.some(cell => cell.decodedValue.trim()))
|
||||
.map(row => ({
|
||||
version: 1 as const,
|
||||
id: `candidate-${safeId(table.id)}-${row.index}`,
|
||||
source: { fileName: document.file.name, tableIndex: table.index, rowIndex: row.index },
|
||||
fields: row.cells.map(cell => ({
|
||||
key: `column-${cell.source.columnIndex ?? 0}`,
|
||||
label: header?.headers[cell.source.columnIndex ?? 0] || `Spalte ${(cell.source.columnIndex ?? 0) + 1}`,
|
||||
originalValue: cell.rawValue,
|
||||
decodedValue: cell.decodedValue,
|
||||
source: { ...cell.source, fileName: document.file.name }
|
||||
}))
|
||||
}));
|
||||
});
|
||||
return { version: 1, candidates, diagnostics: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export const sourceExtractors: SourceExtractor[] = [
|
||||
defenderForIotSourceExtractor,
|
||||
pingCastleSourceExtractor,
|
||||
cisCatSourceExtractor,
|
||||
purpleKnightHtmlSourceExtractor,
|
||||
purpleKnightSummarySourceExtractor,
|
||||
maesterHtmlSourceExtractor,
|
||||
nativeObjectSourceExtractor,
|
||||
genericTableSourceExtractor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
import type { JsonValue } from '../domain/canonical-model';
|
||||
|
||||
// Deliberately a data grammar, not a JavaScript interpreter. No names, calls,
|
||||
// getters, spreads, computed properties or template substitutions are accepted.
|
||||
export const STATIC_REPORT_LIMITS = { depth: 128, values: 1_000_000, characters: 16 * 1024 * 1024, string: 1_000_000 } as const;
|
||||
|
||||
export class StaticReportLiteralError extends Error {}
|
||||
|
||||
export function readStaticReportObject(text: string, start: number): { value: Record<string, JsonValue>; end: number } {
|
||||
let position = start;
|
||||
let values = 0;
|
||||
const fail = (): never => { throw new StaticReportLiteralError('Unsupported, malformed or oversized static report data.'); };
|
||||
const check = () => { if (position - start > STATIC_REPORT_LIMITS.characters) fail(); };
|
||||
const whitespace = () => {
|
||||
while (position < text.length) {
|
||||
check();
|
||||
if (/\s/.test(text[position])) { position++; continue; }
|
||||
if (text.startsWith('//', position)) {
|
||||
while (position < text.length && !/[\r\n]/.test(text[position])) { position++; check(); }
|
||||
} else if (text.startsWith('/*', position)) {
|
||||
const end = text.indexOf('*/', position + 2);
|
||||
if (end < 0) fail();
|
||||
position = end + 2;
|
||||
} else break;
|
||||
}
|
||||
check();
|
||||
};
|
||||
const string = (): string => {
|
||||
const quote = text[position++];
|
||||
let output = '';
|
||||
while (position < text.length) {
|
||||
check();
|
||||
if (output.length > STATIC_REPORT_LIMITS.string) fail();
|
||||
let character = text[position++];
|
||||
if (character === quote) return output;
|
||||
if (quote === '`' && character === '$' && text[position] === '{') fail();
|
||||
if (character === '\\') {
|
||||
character = text[position++];
|
||||
if (character === undefined) fail();
|
||||
if (character === '\r' || character === '\n') {
|
||||
if (character === '\r' && text[position] === '\n') position++;
|
||||
continue;
|
||||
}
|
||||
const escapes: Record<string, string> = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v', '0': '\0' };
|
||||
if (character === 'x' || character === 'u') {
|
||||
const braced = character === 'u' && text[position] === '{';
|
||||
if (braced) position++;
|
||||
const count = braced ? text.indexOf('}', position) - position : character === 'x' ? 2 : 4;
|
||||
if (count < 1 || count > 6) fail();
|
||||
const hex = text.slice(position, position + count);
|
||||
if (hex.length !== count || !/^[\da-f]+$/i.test(hex)) fail();
|
||||
const code = Number.parseInt(hex, 16);
|
||||
if (code > 0x10ffff) fail();
|
||||
output += String.fromCodePoint(code);
|
||||
position += count + (braced ? 1 : 0);
|
||||
} else {
|
||||
if (/[1-9]/.test(character) || (character === '0' && /\d/.test(text[position] ?? ''))) fail();
|
||||
output += escapes[character] ?? character;
|
||||
}
|
||||
} else {
|
||||
if (quote !== '`' && character.charCodeAt(0) < 32) fail();
|
||||
if (quote === '`' && character === '\r') {
|
||||
if (text[position] === '\n') position++;
|
||||
character = '\n';
|
||||
}
|
||||
output += character;
|
||||
}
|
||||
}
|
||||
return fail();
|
||||
};
|
||||
const value = (depth: number): JsonValue => {
|
||||
if (depth > STATIC_REPORT_LIMITS.depth || ++values > STATIC_REPORT_LIMITS.values) fail();
|
||||
whitespace();
|
||||
const character = text[position];
|
||||
if (character === '"' || character === "'" || character === '`') return string();
|
||||
if (character === '{') {
|
||||
position++;
|
||||
const result: Record<string, JsonValue> = Object.create(null);
|
||||
whitespace();
|
||||
while (text[position] !== '}') {
|
||||
whitespace();
|
||||
const quoted = text[position] === '"' || text[position] === "'";
|
||||
const key = quoted ? string() : /^[A-Za-z_$][\w$]*/.exec(text.slice(position, position + 256))?.[0];
|
||||
if (key === undefined) return fail();
|
||||
if (!quoted) position += key.length;
|
||||
if (Object.hasOwn(result, key)) fail();
|
||||
whitespace();
|
||||
if (text[position++] !== ':') fail();
|
||||
result[key] = value(depth + 1);
|
||||
whitespace();
|
||||
if (text[position] === '}') break;
|
||||
if (text[position++] !== ',') fail();
|
||||
whitespace();
|
||||
}
|
||||
position++;
|
||||
return result;
|
||||
}
|
||||
if (character === '[') {
|
||||
position++;
|
||||
const result: JsonValue[] = [];
|
||||
whitespace();
|
||||
while (text[position] !== ']') {
|
||||
result.push(value(depth + 1));
|
||||
whitespace();
|
||||
if (text[position] === ']') break;
|
||||
if (text[position++] !== ',') fail();
|
||||
whitespace();
|
||||
}
|
||||
position++;
|
||||
return result;
|
||||
}
|
||||
const token = /^(?:true|false|null|!\s*[01]|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(text.slice(position, position + 128))?.[0];
|
||||
if (!token) return fail();
|
||||
position += token.length;
|
||||
if (token === 'null') return null;
|
||||
if (token === 'true' || /^!\s*0$/.test(token)) return true;
|
||||
if (token === 'false' || /^!\s*1$/.test(token)) return false;
|
||||
const numeric = Number(token);
|
||||
return Number.isFinite(numeric) ? numeric : fail();
|
||||
};
|
||||
whitespace();
|
||||
if (text[position] !== '{') fail();
|
||||
const result = value(0) as Record<string, JsonValue>;
|
||||
whitespace();
|
||||
// Reject an object used as the prefix of a call, member access or expression.
|
||||
if (position < text.length && !/[,;)]/.test(text[position])) fail();
|
||||
return { value: result, end: position };
|
||||
}
|
||||
|
||||
export type StaticReportBindingMatcher = (script: string, position: number) => { start: number; binding: string } | undefined;
|
||||
|
||||
/** Locate assignment values in script text, skipping quoted text and comments.
|
||||
* Optional binding grammars are supplied by registered source extractors. */
|
||||
export function* assignedReportObjects(script: string, bindingMatcher?: StaticReportBindingMatcher): Generator<{ start: number; binding: string }> {
|
||||
let position = 0;
|
||||
let previous = '';
|
||||
let canEndExpression = false;
|
||||
let quoteDepth = 0;
|
||||
const skipQuoted = (quote: string): void => {
|
||||
if (++quoteDepth > STATIC_REPORT_LIMITS.depth) throw new StaticReportLiteralError('Static report script nesting limit exceeded.');
|
||||
try {
|
||||
position++;
|
||||
while (position < script.length) {
|
||||
const character = script[position++];
|
||||
if (character === '\\') position++;
|
||||
else if (character === quote) return;
|
||||
else if (quote === '`' && character === '$' && script[position] === '{') {
|
||||
position++;
|
||||
skipCode(true);
|
||||
}
|
||||
}
|
||||
} finally { quoteDepth--; }
|
||||
};
|
||||
const skipCode = (inTemplate: boolean): void => {
|
||||
let depth = 1;
|
||||
while (position < script.length && depth) {
|
||||
const character = script[position];
|
||||
if ('"\'`'.includes(character)) skipQuoted(character);
|
||||
else if (script.startsWith('//', position)) {
|
||||
while (position < script.length && !/[\r\n]/.test(script[position])) position++;
|
||||
} else if (script.startsWith('/*', position)) {
|
||||
const end = script.indexOf('*/', position + 2);
|
||||
position = end < 0 ? script.length : end + 2;
|
||||
} else {
|
||||
position++;
|
||||
if (inTemplate && character === '{') depth++;
|
||||
if (inTemplate && character === '}') depth--;
|
||||
}
|
||||
}
|
||||
};
|
||||
while (position < script.length) {
|
||||
const character = script[position];
|
||||
if (/\s/.test(character)) { position++; continue; }
|
||||
if ('"\'`'.includes(character)) { skipQuoted(character); canEndExpression = true; previous = ''; continue; }
|
||||
if (script.startsWith('//', position)) {
|
||||
while (position < script.length && !/[\r\n]/.test(script[position])) position++;
|
||||
continue;
|
||||
}
|
||||
if (script.startsWith('/*', position)) {
|
||||
const end = script.indexOf('*/', position + 2);
|
||||
position = end < 0 ? script.length : end + 2;
|
||||
continue;
|
||||
}
|
||||
if (character === '/' && !canEndExpression) {
|
||||
position++;
|
||||
let inClass = false;
|
||||
while (position < script.length) {
|
||||
const next = script[position++];
|
||||
if (next === '\\') position++;
|
||||
else if (next === '[') inClass = true;
|
||||
else if (next === ']') inClass = false;
|
||||
else if (next === '/' && !inClass) break;
|
||||
else if (/[\r\n]/.test(next)) break;
|
||||
}
|
||||
canEndExpression = true;
|
||||
previous = '';
|
||||
continue;
|
||||
}
|
||||
const identifier = /^[A-Za-z_$][\w$]*/.exec(script.slice(position, position + 256))?.[0];
|
||||
if (identifier) {
|
||||
const assignment = bindingMatcher?.(script, position);
|
||||
if (assignment) {
|
||||
if (assignment.start <= position || assignment.start > script.length) throw new StaticReportLiteralError('Invalid assignment locator.');
|
||||
position = assignment.start;
|
||||
yield assignment;
|
||||
previous = '';
|
||||
continue;
|
||||
}
|
||||
previous = identifier;
|
||||
position += identifier.length;
|
||||
canEndExpression = !/^(?:return|throw|case|typeof|void|delete|new|yield|await|in|of|instanceof)$/.test(identifier);
|
||||
continue;
|
||||
}
|
||||
if (!bindingMatcher && character === '=' && previous && !/[=!<>]/.test(script[position - 1] ?? '') && !/[=>]/.test(script[position + 1] ?? '')) {
|
||||
const next = /^\s*/.exec(script.slice(position + 1))![0].length + position + 1;
|
||||
if (script[next] === '{') yield { start: next, binding: previous };
|
||||
}
|
||||
previous = '';
|
||||
canEndExpression = /[\d)\]}]/.test(character);
|
||||
position++;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue