Harden app for release readiness
All checks were successful
Build Windows App / build-windows (push) Successful in 25m17s

This commit is contained in:
MrSphay
2026-05-02 01:01:57 +02:00
parent 77a69c180c
commit a35acb3ea9
7 changed files with 116 additions and 12 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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",

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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);
crypto.getRandomValues(array);
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 {