Harden app for release readiness
All checks were successful
Build Windows App / build-windows (push) Successful in 25m17s
All checks were successful
Build Windows App / build-windows (push) Successful in 25m17s
This commit is contained in:
@@ -42,6 +42,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: npm audit --omit=dev --audit-level=high
|
||||
|
||||
- name: Build Windows installer
|
||||
run: npm run dist:win
|
||||
|
||||
@@ -58,7 +61,8 @@ jobs:
|
||||
- name: Publish to Gitea package registry
|
||||
shell: bash
|
||||
run: |
|
||||
package_version="$(node -p "require('./package.json').version")-${GITHUB_SHA::7}"
|
||||
app_version="$(node -p "require('./package.json').version")"
|
||||
package_version="${app_version}-${GITHUB_SHA::7}"
|
||||
|
||||
for artifact in release/*; do
|
||||
[ -f "$artifact" ] || continue
|
||||
@@ -68,3 +72,18 @@ jobs:
|
||||
--upload-file "$artifact" \
|
||||
"https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/${package_version}/${file_name}"
|
||||
done
|
||||
|
||||
latest_url="https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/latest"
|
||||
curl --silent --show-error --user "MrSphay:${REGISTRY_TOKEN}" --request DELETE "${latest_url}" || true
|
||||
|
||||
mkdir -p package-latest
|
||||
cp "release/EnvHelper-${app_version}-setup-x64.exe" "package-latest/EnvHelper-setup-x64.exe"
|
||||
cp "release/EnvHelper-${app_version}-portable-x64.exe" "package-latest/EnvHelper-portable-x64.exe"
|
||||
|
||||
for artifact in package-latest/*; do
|
||||
file_name="$(basename "$artifact")"
|
||||
curl --fail-with-body \
|
||||
--user "MrSphay:${REGISTRY_TOKEN}" \
|
||||
--upload-file "$artifact" \
|
||||
"${latest_url}/${file_name}"
|
||||
done
|
||||
|
||||
25
README.md
25
README.md
@@ -152,14 +152,23 @@ Manual defaults can always be added. They override automatically detected defaul
|
||||
|
||||
## Downloads and Artifacts
|
||||
|
||||
The Windows build produces two executable artifacts:
|
||||
The current Windows build can be downloaded directly from the Gitea Generic Package registry:
|
||||
|
||||
| Variant | Download |
|
||||
| --- | --- |
|
||||
| Installer | [EnvHelper-setup-x64.exe](https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/latest/EnvHelper-setup-x64.exe) |
|
||||
| Portable | [EnvHelper-portable-x64.exe](https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/latest/EnvHelper-portable-x64.exe) |
|
||||
|
||||
Private package downloads may require an active Gitea session or a token with package read access.
|
||||
|
||||
Each build also produces versioned executable artifacts:
|
||||
|
||||
```text
|
||||
EnvHelper-0.1.0-setup-x64.exe
|
||||
EnvHelper-0.1.0-portable-x64.exe
|
||||
```
|
||||
|
||||
The files are published by the Gitea Runner as an Actions artifact and as a Generic Package.
|
||||
The files are published by the Gitea Runner as an Actions artifact, as an immutable `version-sha` Generic Package, and as the moving `latest` Generic Package used by the links above.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" alt="-----------------------------------------------------" width="100%"></p>
|
||||
|
||||
@@ -237,6 +246,18 @@ The generated output is committed as `README.md` so Gitea can render it directly
|
||||
|
||||
EnvHelper generates values locally in the renderer using Web Crypto. It is a helper for `.env` templates and is not a replacement for a central secret manager in production infrastructure.
|
||||
|
||||
Security posture:
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
| Secret generation | Uses `crypto.getRandomValues` and `crypto.randomUUID` |
|
||||
| Renderer isolation | Electron `contextIsolation` and sandbox are enabled |
|
||||
| Node access | `nodeIntegration` is disabled in the renderer |
|
||||
| Navigation | New windows and renderer navigation are blocked |
|
||||
| Content policy | The app ships with a restrictive Content Security Policy |
|
||||
| Default storage | Sensitive manual defaults such as passwords, tokens, and API keys are not persisted |
|
||||
| External services | No `.env` input or generated secret is sent to external services |
|
||||
|
||||
### Windows Defender and SmartScreen
|
||||
|
||||
Windows may block or delay apps from unknown publishers. This is usually caused by missing reputation or by the absence of a trusted code-signing certificate.
|
||||
|
||||
25
blueprint.md
25
blueprint.md
@@ -121,14 +121,23 @@ Manual defaults can always be added. They override automatically detected defaul
|
||||
{{ template:section-line }}
|
||||
## Downloads and Artifacts
|
||||
|
||||
The Windows build produces two executable artifacts:
|
||||
The current Windows build can be downloaded directly from the Gitea Generic Package registry:
|
||||
|
||||
| Variant | Download |
|
||||
| --- | --- |
|
||||
| Installer | [EnvHelper-setup-x64.exe](https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/latest/EnvHelper-setup-x64.exe) |
|
||||
| Portable | [EnvHelper-portable-x64.exe](https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/latest/EnvHelper-portable-x64.exe) |
|
||||
|
||||
Private package downloads may require an active Gitea session or a token with package read access.
|
||||
|
||||
Each build also produces versioned executable artifacts:
|
||||
|
||||
```text
|
||||
EnvHelper-0.1.0-setup-x64.exe
|
||||
EnvHelper-0.1.0-portable-x64.exe
|
||||
```
|
||||
|
||||
The files are published by the Gitea Runner as an Actions artifact and as a Generic Package.
|
||||
The files are published by the Gitea Runner as an Actions artifact, as an immutable `version-sha` Generic Package, and as the moving `latest` Generic Package used by the links above.
|
||||
|
||||
{{ template:section-line }}
|
||||
## Development
|
||||
@@ -202,6 +211,18 @@ The generated output is committed as `README.md` so Gitea can render it directly
|
||||
|
||||
EnvHelper generates values locally in the renderer using Web Crypto. It is a helper for `.env` templates and is not a replacement for a central secret manager in production infrastructure.
|
||||
|
||||
Security posture:
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
| Secret generation | Uses `crypto.getRandomValues` and `crypto.randomUUID` |
|
||||
| Renderer isolation | Electron `contextIsolation` and sandbox are enabled |
|
||||
| Node access | `nodeIntegration` is disabled in the renderer |
|
||||
| Navigation | New windows and renderer navigation are blocked |
|
||||
| Content policy | The app ships with a restrictive Content Security Policy |
|
||||
| Default storage | Sensitive manual defaults such as passwords, tokens, and API keys are not persisted |
|
||||
| External services | No `.env` input or generated secret is sent to external services |
|
||||
|
||||
### Windows Defender and SmartScreen
|
||||
|
||||
Windows may block or delay apps from unknown publishers. This is usually caused by missing reputation or by the absence of a trusted code-signing certificate.
|
||||
|
||||
@@ -6,6 +6,8 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
app.setAppUserModelId("de.wilkensxl.envhelper");
|
||||
|
||||
async function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1220,
|
||||
@@ -19,11 +21,24 @@ async function createWindow() {
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
nodeIntegration: false,
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
devTools: !app.isPackaged
|
||||
}
|
||||
});
|
||||
|
||||
Menu.setApplicationMenu(null);
|
||||
win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||
win.webContents.on("will-navigate", (event, url) => {
|
||||
const currentUrl = win.webContents.getURL();
|
||||
if (!currentUrl || url === currentUrl || (!app.isPackaged && url.startsWith("http://127.0.0.1:5173"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
if (!app.isPackaged) {
|
||||
await win.loadURL("http://127.0.0.1:5173");
|
||||
@@ -56,6 +71,10 @@ ipcMain.handle("envhelper:open-file", async (event) => {
|
||||
});
|
||||
|
||||
ipcMain.handle("envhelper:save-file", async (event, content: string) => {
|
||||
if (typeof content !== "string") {
|
||||
throw new TypeError("Save content must be a string.");
|
||||
}
|
||||
|
||||
const owner = BrowserWindow.fromWebContents(event.sender);
|
||||
const options = {
|
||||
defaultPath: ".env",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; object-src 'none'; base-uri 'none'; form-action 'none'"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>EnvHelper</title>
|
||||
</head>
|
||||
|
||||
@@ -260,12 +260,17 @@ function readDefaults(): EnvDefault[] {
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as EnvDefault[];
|
||||
return Array.isArray(parsed) ? parsed : initialDefaults;
|
||||
return Array.isArray(parsed) ? parsed.filter((entry) => entry.key && typeof entry.value === "string" && canPersistDefault(entry.key)) : initialDefaults;
|
||||
} catch {
|
||||
return initialDefaults;
|
||||
}
|
||||
}
|
||||
|
||||
function canPersistDefault(key: string): boolean {
|
||||
const signal = normalizeKey(key);
|
||||
return !["PASSWORD", "SECRET", "TOKEN", "PRIVATE_KEY", "API_KEY", "ACCESS_KEY"].some((marker) => signal.includes(marker));
|
||||
}
|
||||
|
||||
function readIgnoredAutoDefaults(): string[] {
|
||||
const stored = localStorage.getItem("envhelper-ignored-auto-defaults");
|
||||
if (!stored) {
|
||||
@@ -518,7 +523,7 @@ export default function App() {
|
||||
}, [language]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("envhelper-defaults", JSON.stringify(manualDefaults));
|
||||
localStorage.setItem("envhelper-defaults", JSON.stringify(manualDefaults.filter((entry) => canPersistDefault(entry.key))));
|
||||
}, [manualDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
19
src/env.ts
19
src/env.ts
@@ -359,13 +359,28 @@ function randomBytes(length: number): Uint8Array {
|
||||
function randomInteger(min: number, max: number): number {
|
||||
const range = max - min + 1;
|
||||
const array = new Uint32Array(1);
|
||||
const limit = Math.floor(0x100000000 / range) * range;
|
||||
|
||||
do {
|
||||
crypto.getRandomValues(array);
|
||||
} while (array[0] >= limit);
|
||||
|
||||
return min + (array[0] % range);
|
||||
}
|
||||
|
||||
function randomString(length: number, alphabet: string): string {
|
||||
const bytes = randomBytes(length);
|
||||
return [...bytes].map((byte) => alphabet[byte % alphabet.length]).join("");
|
||||
let value = "";
|
||||
const limit = Math.floor(256 / alphabet.length) * alphabet.length;
|
||||
|
||||
while (value.length < length) {
|
||||
for (const byte of randomBytes(length - value.length)) {
|
||||
if (byte < limit) {
|
||||
value += alphabet[byte % alphabet.length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array): string {
|
||||
|
||||
Reference in New Issue
Block a user