lula2 0.7.5 → 0.8.4-nightly.0
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/{DsuS1uUo.js → BNu54jRO.js} +1 -1
- package/dist/_app/immutable/chunks/Bct7KINo.js +1 -0
- package/dist/_app/immutable/chunks/CC6oS456.js +1 -0
- package/dist/_app/immutable/chunks/CfYuj7nz.js +79 -0
- package/dist/_app/immutable/chunks/{BIdjJ0zz.js → D9WVNv7O.js} +1 -1
- package/dist/_app/immutable/chunks/DHuA7MQr.js +1 -0
- package/dist/_app/immutable/chunks/DSxRA67V.js +2 -0
- package/dist/_app/immutable/chunks/Dpd5zUJG.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.DMutRaVE.js} +2 -2
- package/dist/_app/immutable/entry/start.BZfBvC8E.js +1 -0
- package/dist/_app/immutable/nodes/{0.CGKh5y4X.js → 0.jGNluNZR.js} +2 -2
- package/dist/_app/immutable/nodes/1.DO1jbRyK.js +1 -0
- package/dist/_app/immutable/nodes/{2.Hrl6uq-b.js → 2.CeUzIeUq.js} +1 -1
- package/dist/_app/immutable/nodes/3.BcMLTtzX.js +1 -0
- package/dist/_app/immutable/nodes/{4.DAVWsDkK.js → 4.CkamcV91.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 +127 -128
- 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.BZfBvC8E.js">
|
|
10
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/Dpd5zUJG.js">
|
|
11
|
+
<link rel="modulepreload" href="/_app/immutable/chunks/DHuA7MQr.js">
|
|
12
|
+
<link rel="modulepreload" href="/_app/immutable/entry/app.DMutRaVE.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_1gac0aa = {
|
|
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.BZfBvC8E.js"),
|
|
30
|
+
import("/_app/immutable/entry/app.DMutRaVE.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,129 +1,128 @@
|
|
|
1
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
2
|
+
"name": "lula2",
|
|
3
|
+
"version": "0.8.4-nightly.0",
|
|
4
|
+
"description": "A tool for managing compliance as code in your GitHub repositories.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"lula2": "./dist/lula2"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=22.20.0"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/defenseunicorns/lula.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"compliance",
|
|
20
|
+
"devops",
|
|
21
|
+
"devsecops"
|
|
22
|
+
],
|
|
23
|
+
"author": "Defense Unicorns",
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/defenseunicorns/lula/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/defenseunicorns/lula#readme",
|
|
29
|
+
"files": [
|
|
30
|
+
"/src",
|
|
31
|
+
"/dist",
|
|
32
|
+
"!src/**/*.test.ts",
|
|
33
|
+
"!dist/**/*.test.js*",
|
|
34
|
+
"!dist/**/*.test.d.ts*"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@octokit/rest": "22.0.1",
|
|
38
|
+
"@types/ws": "8.18.1",
|
|
39
|
+
"commander": "14.0.2",
|
|
40
|
+
"cors": "2.8.5",
|
|
41
|
+
"csv-parse": "6.1.0",
|
|
42
|
+
"express": "5.2.1",
|
|
43
|
+
"express-rate-limit": "8.2.1",
|
|
44
|
+
"flowbite": "4.0.1",
|
|
45
|
+
"glob": "13.0.0",
|
|
46
|
+
"isomorphic-git": "1.36.0",
|
|
47
|
+
"js-yaml": "4.1.1",
|
|
48
|
+
"multer": "2.0.2",
|
|
49
|
+
"open": "11.0.0",
|
|
50
|
+
"undici": "7.16.0",
|
|
51
|
+
"ws": "8.18.3",
|
|
52
|
+
"xlsx-republish": "0.20.3",
|
|
53
|
+
"yaml": "2.8.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@commitlint/cli": "^20.0.0",
|
|
57
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
58
|
+
"@defenseunicorns/eslint-config": "^1.1.2",
|
|
59
|
+
"@eslint/compat": "^2.0.0",
|
|
60
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
61
|
+
"@eslint/js": "^9.35.0",
|
|
62
|
+
"@playwright/test": "^1.55.0",
|
|
63
|
+
"@sveltejs/adapter-static": "^3.0.9",
|
|
64
|
+
"@sveltejs/kit": "^2.37.1",
|
|
65
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.4",
|
|
66
|
+
"@tailwindcss/vite": "^4.1.13",
|
|
67
|
+
"@types/cors": "^2.8.19",
|
|
68
|
+
"@types/express": "^5.0.3",
|
|
69
|
+
"@types/js-yaml": "^4.0.9",
|
|
70
|
+
"@types/jsdom": "^27.0.0",
|
|
71
|
+
"@types/multer": "^2.0.0",
|
|
72
|
+
"@types/node": "^25.0.0",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^8.42.0",
|
|
74
|
+
"@typescript-eslint/parser": "^8.42.0",
|
|
75
|
+
"@vitest/browser": "^4.0.1",
|
|
76
|
+
"@vitest/coverage-v8": "^4.0.1",
|
|
77
|
+
"carbon-icons-svelte": "^13.5.0",
|
|
78
|
+
"carbon-preprocess-svelte": "^0.11.11",
|
|
79
|
+
"concurrently": "^9.2.1",
|
|
80
|
+
"esbuild": "^0.27.0",
|
|
81
|
+
"eslint": "^9.35.0",
|
|
82
|
+
"eslint-config-prettier": "^10.1.8",
|
|
83
|
+
"eslint-plugin-jsdoc": "^61.0.0",
|
|
84
|
+
"eslint-plugin-svelte": "^3.12.2",
|
|
85
|
+
"globals": "^16.3.0",
|
|
86
|
+
"husky": "^9.1.7",
|
|
87
|
+
"jsdom": "^27.0.0",
|
|
88
|
+
"playwright": "^1.55.0",
|
|
89
|
+
"prettier": "3.7.4",
|
|
90
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
91
|
+
"semantic-release": "^25.0.1",
|
|
92
|
+
"shellcheck": "^4.1.0",
|
|
93
|
+
"svelte": "^5.38.7",
|
|
94
|
+
"svelte-check": "^4.3.1",
|
|
95
|
+
"tailwind-merge": "^3.3.1",
|
|
96
|
+
"tailwindcss": "^4.1.13",
|
|
97
|
+
"tsx": "^4.20.5",
|
|
98
|
+
"typescript": "5.9.3",
|
|
99
|
+
"typescript-eslint": "^8.42.0",
|
|
100
|
+
"vite": "^7.1.4",
|
|
101
|
+
"vitest": "^4.0.1",
|
|
102
|
+
"vitest-browser-svelte": "^2.0.0"
|
|
103
|
+
},
|
|
104
|
+
"release": {
|
|
105
|
+
"branches": [
|
|
106
|
+
"main",
|
|
107
|
+
"next"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"scripts": {
|
|
111
|
+
"dev": "vite dev --port 5173",
|
|
112
|
+
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
113
|
+
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
114
|
+
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
115
|
+
"build:svelte": "vite build",
|
|
116
|
+
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:xlsx-republish --external:csv-parse",
|
|
117
|
+
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
118
|
+
"preview": "vite preview",
|
|
119
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
120
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
121
|
+
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
122
|
+
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
123
|
+
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
124
|
+
"test": "npm run test:unit -- --run --coverage",
|
|
125
|
+
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
126
|
+
"test:unit": "vitest"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -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}
|