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
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
|
- name: Audit production dependencies
|
||||||
|
run: npm audit --omit=dev --audit-level=high
|
||||||
|
|
||||||
- name: Build Windows installer
|
- name: Build Windows installer
|
||||||
run: npm run dist:win
|
run: npm run dist:win
|
||||||
|
|
||||||
@@ -58,7 +61,8 @@ jobs:
|
|||||||
- name: Publish to Gitea package registry
|
- name: Publish to Gitea package registry
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
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
|
for artifact in release/*; do
|
||||||
[ -f "$artifact" ] || continue
|
[ -f "$artifact" ] || continue
|
||||||
@@ -68,3 +72,18 @@ jobs:
|
|||||||
--upload-file "$artifact" \
|
--upload-file "$artifact" \
|
||||||
"https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/${package_version}/${file_name}"
|
"https://git.wilkensxl.de/api/packages/MrSphay/generic/envhelper/${package_version}/${file_name}"
|
||||||
done
|
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
|
## 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
|
```text
|
||||||
EnvHelper-0.1.0-setup-x64.exe
|
EnvHelper-0.1.0-setup-x64.exe
|
||||||
EnvHelper-0.1.0-portable-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>
|
<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.
|
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 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.
|
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 }}
|
{{ template:section-line }}
|
||||||
## Downloads and Artifacts
|
## 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
|
```text
|
||||||
EnvHelper-0.1.0-setup-x64.exe
|
EnvHelper-0.1.0-setup-x64.exe
|
||||||
EnvHelper-0.1.0-portable-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 }}
|
{{ template:section-line }}
|
||||||
## Development
|
## 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.
|
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 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.
|
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));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
app.setAppUserModelId("de.wilkensxl.envhelper");
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1220,
|
width: 1220,
|
||||||
@@ -19,11 +21,24 @@ async function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false
|
nodeIntegration: false,
|
||||||
|
sandbox: true,
|
||||||
|
webSecurity: true,
|
||||||
|
allowRunningInsecureContent: false,
|
||||||
|
devTools: !app.isPackaged
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Menu.setApplicationMenu(null);
|
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) {
|
if (!app.isPackaged) {
|
||||||
await win.loadURL("http://127.0.0.1:5173");
|
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) => {
|
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 owner = BrowserWindow.fromWebContents(event.sender);
|
||||||
const options = {
|
const options = {
|
||||||
defaultPath: ".env",
|
defaultPath: ".env",
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EnvHelper</title>
|
<title>EnvHelper</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -260,12 +260,17 @@ function readDefaults(): EnvDefault[] {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored) as EnvDefault[];
|
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 {
|
} catch {
|
||||||
return initialDefaults;
|
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[] {
|
function readIgnoredAutoDefaults(): string[] {
|
||||||
const stored = localStorage.getItem("envhelper-ignored-auto-defaults");
|
const stored = localStorage.getItem("envhelper-ignored-auto-defaults");
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
@@ -518,7 +523,7 @@ export default function App() {
|
|||||||
}, [language]);
|
}, [language]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("envhelper-defaults", JSON.stringify(manualDefaults));
|
localStorage.setItem("envhelper-defaults", JSON.stringify(manualDefaults.filter((entry) => canPersistDefault(entry.key))));
|
||||||
}, [manualDefaults]);
|
}, [manualDefaults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
21
src/env.ts
21
src/env.ts
@@ -359,13 +359,28 @@ function randomBytes(length: number): Uint8Array {
|
|||||||
function randomInteger(min: number, max: number): number {
|
function randomInteger(min: number, max: number): number {
|
||||||
const range = max - min + 1;
|
const range = max - min + 1;
|
||||||
const array = new Uint32Array(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);
|
return min + (array[0] % range);
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomString(length: number, alphabet: string): string {
|
function randomString(length: number, alphabet: string): string {
|
||||||
const bytes = randomBytes(length);
|
let value = "";
|
||||||
return [...bytes].map((byte) => alphabet[byte % alphabet.length]).join("");
|
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 {
|
function bytesToBase64(bytes: Uint8Array): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user