lula2 0.7.5 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/_app/immutable/assets/{0.DqWJrcPI.css → 0.DLu2XH4u.css} +1 -1
- package/dist/_app/immutable/chunks/BHkKokgA.js +1 -0
- package/dist/_app/immutable/chunks/{DsuS1uUo.js → BNu54jRO.js} +1 -1
- package/dist/_app/immutable/chunks/BzIr_GrC.js +79 -0
- package/dist/_app/immutable/chunks/CC6oS456.js +1 -0
- package/dist/_app/immutable/chunks/DHuA7MQr.js +1 -0
- package/dist/_app/immutable/chunks/{BIdjJ0zz.js → DJTXGU6C.js} +1 -1
- package/dist/_app/immutable/chunks/DSxRA67V.js +2 -0
- package/dist/_app/immutable/chunks/DpCtGpHu.js +1 -0
- package/dist/_app/immutable/chunks/DznG4VMX.js +2 -0
- package/dist/_app/immutable/chunks/Ew6_cz_0.js +1 -0
- package/dist/_app/immutable/chunks/kRA7ZCNG.js +1 -0
- package/dist/_app/immutable/entry/{app.CjycYot0.js → app.CpHUD0XU.js} +2 -2
- package/dist/_app/immutable/entry/start.BavDkynd.js +1 -0
- package/dist/_app/immutable/nodes/{0.CGKh5y4X.js → 0.D5TULpJI.js} +2 -2
- package/dist/_app/immutable/nodes/1.BBgWG9H0.js +1 -0
- package/dist/_app/immutable/nodes/{2.Hrl6uq-b.js → 2.c2WlghKX.js} +1 -1
- package/dist/_app/immutable/nodes/3.hNTFAKFs.js +1 -0
- package/dist/_app/immutable/nodes/{4.DAVWsDkK.js → 4.C7MOPYAO.js} +7 -7
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/ui.js +98 -8
- package/dist/cli/server/index.js +98 -8
- package/dist/cli/server/server.js +98 -8
- package/dist/cli/server/serverState.js +40 -6
- package/dist/cli/server/websocketServer.js +1146 -1056
- package/dist/index.html +11 -11
- package/dist/index.js +98 -8
- package/package.json +2 -2
- package/src/lib/components/controls/MappingCard.svelte +6 -1
- package/src/lib/components/controls/MappingForm.svelte +36 -3
- package/src/lib/components/controls/renderers/EditableFieldRenderer.svelte +57 -0
- package/src/lib/components/controls/tabs/CustomFieldsTab.svelte +1 -0
- package/src/lib/components/controls/tabs/ImplementationTab.svelte +1 -0
- package/src/lib/components/controls/tabs/MappingsTab.svelte +39 -14
- package/src/lib/components/controls/tabs/OverviewTab.svelte +1 -0
- package/src/lib/components/forms/FormField.svelte +65 -3
- package/src/lib/types.ts +2 -1
- package/src/lib/websocket.ts +7 -0
- package/src/routes/control/[id]/+page.svelte +2 -2
- package/dist/_app/immutable/chunks/BI-GirXZ.js +0 -1
- package/dist/_app/immutable/chunks/BSyVkqhj.js +0 -2
- package/dist/_app/immutable/chunks/Cng7c2CG.js +0 -1
- package/dist/_app/immutable/chunks/CxBMFlfX.js +0 -1
- package/dist/_app/immutable/chunks/DArZRX9-.js +0 -65
- package/dist/_app/immutable/chunks/DH2IP9c7.js +0 -1
- package/dist/_app/immutable/chunks/DXSHWIjJ.js +0 -2
- package/dist/_app/immutable/chunks/urFjAlpd.js +0 -1
- package/dist/_app/immutable/entry/start.Bgy9x4Qb.js +0 -1
- package/dist/_app/immutable/nodes/1.D5L7DxSG.js +0 -1
- package/dist/_app/immutable/nodes/3.BoHxdRm3.js +0 -1
package/dist/index.html
CHANGED
|
@@ -6,28 +6,28 @@
|
|
|
6
6
|
<link rel="icon" href="/lula.png" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
|
|
9
|
-
<link rel="modulepreload" href="/_app/immutable/entry/start.
|
|
10
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
11
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
12
|
-
<link rel="modulepreload" href="/_app/immutable/entry/app.
|
|
13
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
14
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
15
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
16
|
-
<link rel="modulepreload" href="/_app/immutable/chunks/
|
|
9
|
+
<link rel="modulepreload" href="/_app/immutable/entry/start.BavDkynd.js">
|
|
10
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BHkKokgA.js">
|
|
11
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DHuA7MQr.js">
|
|
12
|
+
<link rel="modulepreload" href="/_app/immutable/entry/app.CpHUD0XU.js">
|
|
13
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DSxRA67V.js">
|
|
14
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/Ew6_cz_0.js">
|
|
15
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/CC6oS456.js">
|
|
16
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/BNu54jRO.js">
|
|
17
17
|
</head>
|
|
18
18
|
<body data-sveltekit-preload-data="hover">
|
|
19
19
|
<div style="display: contents">
|
|
20
20
|
<script>
|
|
21
21
|
{
|
|
22
|
-
|
|
22
|
+
__sveltekit_11w68nn = {
|
|
23
23
|
base: ""
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
const element = document.currentScript.parentElement;
|
|
27
27
|
|
|
28
28
|
Promise.all([
|
|
29
|
-
import("/_app/immutable/entry/start.
|
|
30
|
-
import("/_app/immutable/entry/app.
|
|
29
|
+
import("/_app/immutable/entry/start.BavDkynd.js"),
|
|
30
|
+
import("/_app/immutable/entry/app.CpHUD0XU.js")
|
|
31
31
|
]).then(([kit, app]) => {
|
|
32
32
|
kit.start(app, element);
|
|
33
33
|
});
|
package/dist/index.js
CHANGED
|
@@ -1669,6 +1669,7 @@ var fileStore_exports = {};
|
|
|
1669
1669
|
__export(fileStore_exports, {
|
|
1670
1670
|
FileStore: () => FileStore
|
|
1671
1671
|
});
|
|
1672
|
+
import { createHash as createHash2 } from "crypto";
|
|
1672
1673
|
import {
|
|
1673
1674
|
existsSync as existsSync2,
|
|
1674
1675
|
promises as fs,
|
|
@@ -1681,7 +1682,6 @@ import {
|
|
|
1681
1682
|
} from "fs";
|
|
1682
1683
|
import * as yaml2 from "js-yaml";
|
|
1683
1684
|
import { join as join2 } from "path";
|
|
1684
|
-
import { createHash as createHash2 } from "crypto";
|
|
1685
1685
|
var FileStore;
|
|
1686
1686
|
var init_fileStore = __esm({
|
|
1687
1687
|
"cli/server/infrastructure/fileStore.ts"() {
|
|
@@ -1701,6 +1701,40 @@ var init_fileStore = __esm({
|
|
|
1701
1701
|
this.refreshControlsCache();
|
|
1702
1702
|
}
|
|
1703
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Update a single mapping in place, preserving file order
|
|
1706
|
+
*/
|
|
1707
|
+
async updateMapping(oldCompositeKey, updatedMapping) {
|
|
1708
|
+
const mappingFiles = this.getAllMappingFiles();
|
|
1709
|
+
for (const file of mappingFiles) {
|
|
1710
|
+
try {
|
|
1711
|
+
const content = readFileSync2(file, "utf8");
|
|
1712
|
+
let mappings = yaml2.load(content) || [];
|
|
1713
|
+
let changed = false;
|
|
1714
|
+
mappings = mappings.map((m) => {
|
|
1715
|
+
const hash = createHash2("sha256").update(JSON.stringify(m)).digest("hex");
|
|
1716
|
+
if (`${m.control_id}:${hash}` === oldCompositeKey) {
|
|
1717
|
+
const clean = { ...updatedMapping };
|
|
1718
|
+
delete clean.hash;
|
|
1719
|
+
changed = true;
|
|
1720
|
+
return clean;
|
|
1721
|
+
}
|
|
1722
|
+
return m;
|
|
1723
|
+
});
|
|
1724
|
+
if (changed) {
|
|
1725
|
+
const yamlContent = yaml2.dump(mappings, {
|
|
1726
|
+
indent: 2,
|
|
1727
|
+
lineWidth: -1,
|
|
1728
|
+
noRefs: true
|
|
1729
|
+
});
|
|
1730
|
+
writeFileSync(file, yamlContent, "utf8");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
console.error(`Error processing mapping file ${file}:`, error);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1704
1738
|
/**
|
|
1705
1739
|
* Get simple filename from control ID
|
|
1706
1740
|
*/
|
|
@@ -1723,8 +1757,8 @@ var init_fileStore = __esm({
|
|
|
1723
1757
|
if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
|
|
1724
1758
|
return;
|
|
1725
1759
|
}
|
|
1726
|
-
const
|
|
1727
|
-
if (!existsSync2(
|
|
1760
|
+
const lulaConfigPath = join2(this.baseDir, "lula.yaml");
|
|
1761
|
+
if (!existsSync2(lulaConfigPath)) {
|
|
1728
1762
|
return;
|
|
1729
1763
|
}
|
|
1730
1764
|
if (!existsSync2(this.controlsDir)) {
|
|
@@ -1883,10 +1917,10 @@ var init_fileStore = __esm({
|
|
|
1883
1917
|
return [];
|
|
1884
1918
|
}
|
|
1885
1919
|
let controlOrder = null;
|
|
1920
|
+
const lulaConfigPath = join2(this.baseDir, "lula.yaml");
|
|
1886
1921
|
try {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
const content = readFileSync2(lulaConfigPath2, "utf8");
|
|
1922
|
+
if (existsSync2(lulaConfigPath)) {
|
|
1923
|
+
const content = readFileSync2(lulaConfigPath, "utf8");
|
|
1890
1924
|
const metadata = yaml2.load(content);
|
|
1891
1925
|
controlOrder = metadata?.controlOrder || null;
|
|
1892
1926
|
}
|
|
@@ -5143,12 +5177,12 @@ import { fileURLToPath } from "url";
|
|
|
5143
5177
|
import { readFileSync as readFileSync4 } from "fs";
|
|
5144
5178
|
init_debug();
|
|
5145
5179
|
init_controlHelpers();
|
|
5146
|
-
init_serverState();
|
|
5147
5180
|
init_gitHistory();
|
|
5181
|
+
init_serverState();
|
|
5148
5182
|
import * as yaml5 from "js-yaml";
|
|
5183
|
+
import crypto2 from "node:crypto";
|
|
5149
5184
|
import { join as join5 } from "path";
|
|
5150
5185
|
import { WebSocket, WebSocketServer } from "ws";
|
|
5151
|
-
import crypto2 from "node:crypto";
|
|
5152
5186
|
var WebSocketManager = class {
|
|
5153
5187
|
wss = null;
|
|
5154
5188
|
clients = /* @__PURE__ */ new Set();
|
|
@@ -5189,6 +5223,62 @@ var WebSocketManager = class {
|
|
|
5189
5223
|
}
|
|
5190
5224
|
break;
|
|
5191
5225
|
}
|
|
5226
|
+
case "update-mapping": {
|
|
5227
|
+
const state = getServerState();
|
|
5228
|
+
if (payload && payload.old_composite_key && payload.mapping) {
|
|
5229
|
+
const oldCompositeKey = payload.old_composite_key;
|
|
5230
|
+
const existing = state.mappingsCache.get(oldCompositeKey);
|
|
5231
|
+
if (!existing) {
|
|
5232
|
+
console.error("Mapping not found for update:", oldCompositeKey);
|
|
5233
|
+
break;
|
|
5234
|
+
}
|
|
5235
|
+
const incoming = payload.mapping;
|
|
5236
|
+
const updated = {
|
|
5237
|
+
...existing,
|
|
5238
|
+
...incoming,
|
|
5239
|
+
control_id: incoming.control_id || existing.control_id,
|
|
5240
|
+
uuid: incoming.uuid || existing.uuid
|
|
5241
|
+
};
|
|
5242
|
+
if (!updated.hash || updated.hash === "") {
|
|
5243
|
+
updated.hash = crypto2.createHash("sha256").update(JSON.stringify({ ...updated, hash: void 0 })).digest("hex");
|
|
5244
|
+
}
|
|
5245
|
+
const oldHash = existing.hash;
|
|
5246
|
+
const oldControlId = existing.control_id;
|
|
5247
|
+
const oldFamily = oldControlId.split("-")[0];
|
|
5248
|
+
const newHash = updated.hash;
|
|
5249
|
+
const newControlId = updated.control_id;
|
|
5250
|
+
const newFamily = newControlId.split("-")[0];
|
|
5251
|
+
const newCompositeKey = `${newControlId}:${newHash}`;
|
|
5252
|
+
await state.fileStore.updateMapping(oldCompositeKey, updated);
|
|
5253
|
+
const entries = Array.from(state.mappingsCache.entries());
|
|
5254
|
+
const oldIndex = entries.findIndex(([key]) => key === oldCompositeKey);
|
|
5255
|
+
if (oldIndex === -1) {
|
|
5256
|
+
state.mappingsCache.delete(oldCompositeKey);
|
|
5257
|
+
state.mappingsCache.set(newCompositeKey, updated);
|
|
5258
|
+
} else {
|
|
5259
|
+
entries[oldIndex] = [newCompositeKey, updated];
|
|
5260
|
+
state.mappingsCache = new Map(entries);
|
|
5261
|
+
}
|
|
5262
|
+
state.mappingsByFamily.get(oldFamily)?.delete(oldHash);
|
|
5263
|
+
state.mappingsByControl.get(oldControlId)?.delete(oldHash);
|
|
5264
|
+
if (!state.mappingsByFamily.has(newFamily)) {
|
|
5265
|
+
state.mappingsByFamily.set(newFamily, /* @__PURE__ */ new Set());
|
|
5266
|
+
}
|
|
5267
|
+
state.mappingsByFamily.get(newFamily).add(newHash);
|
|
5268
|
+
if (!state.mappingsByControl.has(newControlId)) {
|
|
5269
|
+
state.mappingsByControl.set(newControlId, /* @__PURE__ */ new Set());
|
|
5270
|
+
}
|
|
5271
|
+
state.mappingsByControl.get(newControlId).add(newHash);
|
|
5272
|
+
ws.send(
|
|
5273
|
+
JSON.stringify({
|
|
5274
|
+
type: "mapping-updated",
|
|
5275
|
+
payload: { uuid: updated.uuid, success: true }
|
|
5276
|
+
})
|
|
5277
|
+
);
|
|
5278
|
+
this.broadcastState();
|
|
5279
|
+
}
|
|
5280
|
+
break;
|
|
5281
|
+
}
|
|
5192
5282
|
case "refresh-controls": {
|
|
5193
5283
|
const state = getServerState();
|
|
5194
5284
|
state.controlsCache.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lula2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "A tool for managing compliance as code in your GitHub repositories.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lula2": "./dist/lula2"
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"@types/js-yaml": "^4.0.9",
|
|
89
89
|
"@types/jsdom": "^27.0.0",
|
|
90
90
|
"@types/multer": "^2.0.0",
|
|
91
|
-
"@types/node": "^
|
|
91
|
+
"@types/node": "^25.0.0",
|
|
92
92
|
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
|
93
93
|
"@typescript-eslint/parser": "^8.42.0",
|
|
94
94
|
"@vitest/browser": "^4.0.1",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
onDelete?.(mapping.hash!);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
30
31
|
</script>
|
|
31
32
|
|
|
32
33
|
<div
|
|
@@ -75,7 +76,11 @@
|
|
|
75
76
|
<p class="text-sm text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
|
76
77
|
{mapping.justification}
|
|
77
78
|
</p>
|
|
78
|
-
|
|
79
|
+
{#if mapping.cci}
|
|
80
|
+
<div class="mb-4">
|
|
81
|
+
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 tracking-wider mb-2">CCIs:<span class="text-xs font-mono text-gray-500 dark:text-gray-300 ml-2 break-all" title="CCIs">{mapping.cci}</span></h4>
|
|
82
|
+
</div>
|
|
83
|
+
{/if}
|
|
79
84
|
{#if mapping.source_entries && mapping.source_entries.length > 0}
|
|
80
85
|
<div class="mb-4">
|
|
81
86
|
<h4 class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-2">Source References</h4>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
justification: string;
|
|
12
12
|
status: 'planned' | 'implemented' | 'verified';
|
|
13
13
|
source_entries: SourceEntry[];
|
|
14
|
+
cci: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
onCancel: () => void;
|
|
20
21
|
loading?: boolean;
|
|
21
22
|
submitLabel?: string;
|
|
23
|
+
cci?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
let {
|
|
@@ -26,14 +28,16 @@
|
|
|
26
28
|
onSubmit,
|
|
27
29
|
onCancel,
|
|
28
30
|
loading = false,
|
|
29
|
-
submitLabel = 'Create Mapping'
|
|
31
|
+
submitLabel = 'Create Mapping',
|
|
32
|
+
cci
|
|
30
33
|
}: Props = $props();
|
|
31
34
|
|
|
32
35
|
let formData = $state<MappingFormData>({
|
|
33
36
|
uuid: initialData.uuid || '',
|
|
34
37
|
justification: initialData.justification || '',
|
|
35
38
|
status: initialData.status || 'planned',
|
|
36
|
-
source_entries: initialData.source_entries || []
|
|
39
|
+
source_entries: initialData.source_entries || [],
|
|
40
|
+
cci: initialData.cci ?? ''
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
let newLocation = $state('');
|
|
@@ -42,6 +46,25 @@
|
|
|
42
46
|
const statusOptions = ['planned', 'implemented', 'verified'];
|
|
43
47
|
|
|
44
48
|
const isValid = $derived(formData.justification.trim().length > 0);
|
|
49
|
+
|
|
50
|
+
const cciOptions = $derived(
|
|
51
|
+
(cci ?? '')
|
|
52
|
+
.split(';')
|
|
53
|
+
.map((s) => s.trim())
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
let selectedCCIs = $derived(
|
|
59
|
+
(formData.cci ?? '')
|
|
60
|
+
.split(';')
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
$effect(() => {
|
|
66
|
+
formData.cci = selectedCCIs.join('; ');
|
|
67
|
+
});
|
|
45
68
|
|
|
46
69
|
function handleSubmit() {
|
|
47
70
|
if (!isValid || loading) return;
|
|
@@ -54,7 +77,8 @@
|
|
|
54
77
|
uuid: initialData.uuid || '',
|
|
55
78
|
justification: initialData.justification || '',
|
|
56
79
|
status: initialData.status || 'planned',
|
|
57
|
-
source_entries: initialData.source_entries || []
|
|
80
|
+
source_entries: initialData.source_entries || [],
|
|
81
|
+
cci: initialData.cci ?? ''
|
|
58
82
|
};
|
|
59
83
|
newLocation = '';
|
|
60
84
|
newShasum = '';
|
|
@@ -101,6 +125,15 @@
|
|
|
101
125
|
placeholder="Explain how this compliance artifact satisfies the control requirements..."
|
|
102
126
|
required
|
|
103
127
|
/>
|
|
128
|
+
{#if cci}
|
|
129
|
+
<FormField
|
|
130
|
+
id="mapping-cci"
|
|
131
|
+
label="Mapping CCI(s)"
|
|
132
|
+
type="multiselect"
|
|
133
|
+
bind:value={selectedCCIs}
|
|
134
|
+
options={cciOptions}
|
|
135
|
+
/>
|
|
136
|
+
{/if}
|
|
104
137
|
|
|
105
138
|
<FormField
|
|
106
139
|
id="mapping-status"
|
|
@@ -42,6 +42,63 @@
|
|
|
42
42
|
<option value={option}>{option}</option>
|
|
43
43
|
{/each}
|
|
44
44
|
</select>
|
|
45
|
+
{:else if field.ui_type === 'multiselect' && field.options}
|
|
46
|
+
{@const currentValues = Array.isArray(value) ? (value as string[]) : []}
|
|
47
|
+
|
|
48
|
+
<div id={fieldId} class="flex flex-wrap gap-2">
|
|
49
|
+
{#each field.options as option (option)}
|
|
50
|
+
{@const selected = currentValues.includes(option)}
|
|
51
|
+
|
|
52
|
+
<label
|
|
53
|
+
class={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm cursor-pointer transition-colors
|
|
54
|
+
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2
|
|
55
|
+
dark:focus-within:ring-offset-gray-900
|
|
56
|
+
${
|
|
57
|
+
selected
|
|
58
|
+
? 'bg-blue-50 border-blue-500 text-blue-700 dark:bg-blue-900/40 dark:border-blue-400 dark:text-blue-100'
|
|
59
|
+
: 'bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-200'
|
|
60
|
+
}
|
|
61
|
+
${!field.editable ? 'opacity-50 cursor-not-allowed' : ''}
|
|
62
|
+
`}
|
|
63
|
+
>
|
|
64
|
+
<input
|
|
65
|
+
type="checkbox"
|
|
66
|
+
value={option}
|
|
67
|
+
checked={selected}
|
|
68
|
+
disabled={!field.editable}
|
|
69
|
+
onchange={(e: Event) => {
|
|
70
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
71
|
+
const current = Array.isArray(value) ? (value as string[]) : [];
|
|
72
|
+
let updated: string[];
|
|
73
|
+
|
|
74
|
+
if (input.checked) {
|
|
75
|
+
updated = current.includes(option) ? current : [...current, option];
|
|
76
|
+
} else {
|
|
77
|
+
updated = current.filter((v) => v !== option);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
value = updated;
|
|
81
|
+
onChange();
|
|
82
|
+
}}
|
|
83
|
+
class="sr-only"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<span
|
|
87
|
+
class={`inline-flex items-center justify-center w-4 h-4 rounded border text-[10px] font-bold
|
|
88
|
+
${
|
|
89
|
+
selected
|
|
90
|
+
? 'border-blue-500 bg-blue-500 text-white'
|
|
91
|
+
: 'border-gray-400 bg-transparent text-transparent'
|
|
92
|
+
}
|
|
93
|
+
`}
|
|
94
|
+
>
|
|
95
|
+
✓
|
|
96
|
+
</span>
|
|
97
|
+
|
|
98
|
+
<span>{option}</span>
|
|
99
|
+
</label>
|
|
100
|
+
{/each}
|
|
101
|
+
</div>
|
|
45
102
|
{:else if field.ui_type === 'textarea' || field.ui_type === 'long_text'}
|
|
46
103
|
<textarea
|
|
47
104
|
id={fieldId}
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
uuid: '',
|
|
25
25
|
justification: '',
|
|
26
26
|
status: 'planned' as 'planned' | 'implemented' | 'verified',
|
|
27
|
-
source_entries: [] as { location: string; shasum?: string }[]
|
|
27
|
+
source_entries: [] as { location: string; shasum?: string }[],
|
|
28
|
+
cci: ''
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
// Event handlers
|
|
@@ -36,18 +37,22 @@
|
|
|
36
37
|
status: data.status,
|
|
37
38
|
source_entries: data.source_entries,
|
|
38
39
|
uuid: data.uuid || '', // Use the UUID from form or empty for auto-generation,
|
|
39
|
-
hash: ''
|
|
40
|
+
hash: '',
|
|
40
41
|
};
|
|
42
|
+
const mappingForHash = data.cci !== ''
|
|
43
|
+
? { ...mappingData, cci: data.cci }
|
|
44
|
+
: { ...mappingData };
|
|
45
|
+
|
|
46
|
+
|
|
41
47
|
const hash = await fetch('/hash', {
|
|
42
48
|
method: 'POST',
|
|
43
49
|
headers: {
|
|
44
50
|
'Content-Type': 'application/json'
|
|
45
51
|
},
|
|
46
|
-
body: JSON.stringify(
|
|
52
|
+
body: JSON.stringify(mappingForHash)
|
|
47
53
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await wsClient.createMapping(mappingData);
|
|
54
|
+
mappingForHash.hash = (await hash.json()).hash;
|
|
55
|
+
await wsClient.createMapping(mappingForHash);
|
|
51
56
|
resetMappingForm();
|
|
52
57
|
} catch (error) {
|
|
53
58
|
console.error('Failed to create mapping:', error);
|
|
@@ -63,7 +68,8 @@
|
|
|
63
68
|
uuid: '',
|
|
64
69
|
justification: '',
|
|
65
70
|
status: 'planned',
|
|
66
|
-
source_entries: []
|
|
71
|
+
source_entries: [],
|
|
72
|
+
cci: ''
|
|
67
73
|
};
|
|
68
74
|
showNewMappingForm = false;
|
|
69
75
|
editingMapping = null;
|
|
@@ -76,25 +82,41 @@
|
|
|
76
82
|
uuid: mapping.uuid,
|
|
77
83
|
justification: mapping.justification,
|
|
78
84
|
status: mapping.status,
|
|
79
|
-
source_entries: mapping.source_entries || []
|
|
85
|
+
source_entries: mapping.source_entries || [],
|
|
86
|
+
cci: mapping.cci || ''
|
|
80
87
|
};
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
async function handleUpdateMapping(data: typeof newMappingData) {
|
|
84
91
|
if (!editingMapping) return;
|
|
85
|
-
|
|
92
|
+
|
|
86
93
|
try {
|
|
87
94
|
const updatedMapping = {
|
|
88
95
|
...editingMapping,
|
|
89
96
|
uuid: data.uuid || editingMapping.uuid, // Use form UUID or fallback to original
|
|
90
97
|
justification: data.justification,
|
|
91
98
|
status: data.status,
|
|
92
|
-
source_entries: data.source_entries
|
|
99
|
+
source_entries: data.source_entries
|
|
93
100
|
};
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if(data.cci !== undefined) {
|
|
104
|
+
updatedMapping.cci = data.cci;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Compute new hash for the updated mapping on the backend
|
|
108
|
+
const hashResponse = await fetch('/hash', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json'
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({ ...updatedMapping, hash: '' })
|
|
114
|
+
});
|
|
115
|
+
const hashData = await hashResponse.json();
|
|
116
|
+
updatedMapping.hash = hashData.hash;
|
|
117
|
+
|
|
118
|
+
const oldCompositeKey = `${editingMapping.control_id}:${editingMapping.hash!}`;
|
|
119
|
+
await wsClient.updateMapping(oldCompositeKey, updatedMapping);
|
|
98
120
|
|
|
99
121
|
resetMappingForm();
|
|
100
122
|
} catch (error) {
|
|
@@ -134,6 +156,7 @@
|
|
|
134
156
|
onSubmit={handleUpdateMapping}
|
|
135
157
|
onCancel={cancelNewMapping}
|
|
136
158
|
submitLabel="Update Mapping"
|
|
159
|
+
cci={control.cci}
|
|
137
160
|
/>
|
|
138
161
|
</div>
|
|
139
162
|
{:else}
|
|
@@ -158,6 +181,7 @@
|
|
|
158
181
|
onSubmit={handleCreateMapping}
|
|
159
182
|
onCancel={cancelNewMapping}
|
|
160
183
|
submitLabel="Create Mapping"
|
|
184
|
+
cci={control.cci}
|
|
161
185
|
/>
|
|
162
186
|
</div>
|
|
163
187
|
{:else if !editingMapping}
|
|
@@ -182,6 +206,7 @@
|
|
|
182
206
|
onSubmit={handleCreateMapping}
|
|
183
207
|
onCancel={cancelNewMapping}
|
|
184
208
|
submitLabel="Create Mapping"
|
|
209
|
+
cci={control.cci}
|
|
185
210
|
/>
|
|
186
211
|
</div>
|
|
187
212
|
{:else}
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
|
-
interface
|
|
5
|
+
interface BaseProps {
|
|
6
6
|
id: string;
|
|
7
7
|
label: string;
|
|
8
|
-
type?: 'text' | 'textarea' | 'select';
|
|
9
|
-
value: string;
|
|
8
|
+
type?: 'text' | 'textarea' | 'select' | 'multiselect';
|
|
10
9
|
options?: string[];
|
|
11
10
|
rows?: number;
|
|
12
11
|
placeholder?: string;
|
|
@@ -15,6 +14,18 @@
|
|
|
15
14
|
onChange?: () => void;
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
interface SingleValueProps extends BaseProps {
|
|
18
|
+
type?: 'text' | 'textarea' | 'select';
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MultiSelectProps extends BaseProps {
|
|
23
|
+
type: 'multiselect';
|
|
24
|
+
value: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Props = SingleValueProps | MultiSelectProps;
|
|
28
|
+
|
|
18
29
|
let {
|
|
19
30
|
id,
|
|
20
31
|
label,
|
|
@@ -55,6 +66,57 @@
|
|
|
55
66
|
</div>
|
|
56
67
|
{/if}
|
|
57
68
|
</div>
|
|
69
|
+
{:else if type === 'multiselect' && options.length > 0}
|
|
70
|
+
<div class="flex flex-wrap gap-2">
|
|
71
|
+
{#each options as option (option)}
|
|
72
|
+
{@const selected = (value as string[]).includes(option)}
|
|
73
|
+
<label
|
|
74
|
+
class={`inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border text-sm cursor-pointer transition-colors
|
|
75
|
+
focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2
|
|
76
|
+
dark:focus-within:ring-offset-gray-900
|
|
77
|
+
${
|
|
78
|
+
selected
|
|
79
|
+
? 'bg-blue-50 border-blue-500 text-blue-700 dark:bg-blue-900/40 dark:border-blue-400 dark:text-blue-100'
|
|
80
|
+
: 'bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-200'
|
|
81
|
+
}
|
|
82
|
+
${error ? 'border-red-400' : ''}
|
|
83
|
+
`}
|
|
84
|
+
>
|
|
85
|
+
<input
|
|
86
|
+
type="checkbox"
|
|
87
|
+
value={option}
|
|
88
|
+
checked={selected}
|
|
89
|
+
onchange={(e: Event) => {
|
|
90
|
+
const input = e.currentTarget as HTMLInputElement;
|
|
91
|
+
const current = value as string[];
|
|
92
|
+
|
|
93
|
+
if (input.checked) {
|
|
94
|
+
value = current.includes(option) ? current : [...current, option];
|
|
95
|
+
} else {
|
|
96
|
+
value = current.filter((v) => v !== option);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onChange?.();
|
|
100
|
+
}}
|
|
101
|
+
class="sr-only"
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<span
|
|
105
|
+
class={`inline-flex items-center justify-center w-4 h-4 rounded border text-[10px] font-bold
|
|
106
|
+
${
|
|
107
|
+
selected
|
|
108
|
+
? 'border-blue-500 bg-blue-500 text-white'
|
|
109
|
+
: 'border-gray-400 bg-transparent text-transparent'
|
|
110
|
+
}
|
|
111
|
+
`}
|
|
112
|
+
>
|
|
113
|
+
✓
|
|
114
|
+
</span>
|
|
115
|
+
|
|
116
|
+
<span>{option}</span>
|
|
117
|
+
</label>
|
|
118
|
+
{/each}
|
|
119
|
+
</div>
|
|
58
120
|
{:else if type === 'select' && options.length > 0}
|
|
59
121
|
<div class="relative">
|
|
60
122
|
<select
|
package/src/lib/types.ts
CHANGED
package/src/lib/websocket.ts
CHANGED
|
@@ -355,6 +355,13 @@ class WebSocketClient {
|
|
|
355
355
|
return this.sendCommand('create-mapping', mapping);
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
async updateMapping(oldCompositeKey: string, mapping: Mapping) {
|
|
359
|
+
return this.sendCommand('update-mapping', {
|
|
360
|
+
old_composite_key: oldCompositeKey,
|
|
361
|
+
mapping
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
358
365
|
async deleteMapping(composite_key: string) {
|
|
359
366
|
return this.sendCommand('delete-mapping', { composite_key });
|
|
360
367
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
<script lang="ts">
|
|
5
5
|
import { goto } from '$app/navigation';
|
|
6
|
-
import { page } from '$app/
|
|
6
|
+
import { page } from '$app/state';
|
|
7
7
|
import { ControlDetailsPanel, ControlsList } from '$components/controls';
|
|
8
8
|
import { wsClient } from '$lib/websocket';
|
|
9
9
|
import { selectedControl } from '$stores/compliance';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
// React to URL parameter changes and fetch control details
|
|
18
18
|
$effect(() => {
|
|
19
|
-
const controlId =
|
|
19
|
+
const controlId = page.params.id;
|
|
20
20
|
if (!controlId) return;
|
|
21
21
|
|
|
22
22
|
const decodedControlId = decodeURIComponent(controlId);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{m as g,t as d,u as c,i as m,v as i,w as b,g as p,x as v,y,z as h}from"./CxBMFlfX.js";function x(n=!1){const s=g,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(n){let a=0,t={};const _=y(()=>{let l=!1;const r=s.s;for(const o in r)r[o]!==t[o]&&(t[o]=r[o],l=!0);return l&&a++,a});f=()=>p(_)}e.b.length&&d(()=>{u(s,f),i(e.b)}),c(()=>{const a=m(()=>e.m.map(b));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}h();export{x as i};
|