Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
persist-credentials: false

- name: Resolve PR context
id: pr-context
shell: bash
env:
GH_TOKEN: ${{ github.token }}
Expand Down Expand Up @@ -193,6 +194,7 @@ jobs:
persist-credentials: false

- name: Resolve PR context
id: public-scan-context
shell: bash
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -211,6 +213,14 @@ jobs:
BASE_REF="$PULL_REQUEST_BASE_REF"
fi
echo "PR_BASE_REF=$BASE_REF" >> "$GITHUB_ENV"
echo "base_ref=$BASE_REF" >> "$GITHUB_OUTPUT"

- name: Checkout trusted public scanner
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ steps.public-scan-context.outputs.base_ref }}
path: trusted-security-base
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
Expand All @@ -229,6 +239,18 @@ jobs:
- name: Run postinstall
run: npm run postinstall || true

- name: Run public sensitive content scan
shell: bash
run: |
set -euo pipefail
TRUSTED_SCANNER="trusted-security-base/scripts/security/check-public-sensitive-content.js"
if [ -f "$TRUSTED_SCANNER" ]; then
node "$TRUSTED_SCANNER" "$PWD"
else
echo "::warning::Trusted public scanner is missing on ${PR_BASE_REF}; using PR scanner for bootstrap."
node scripts/security/check-public-sensitive-content.js "$PWD"
fi

- name: Install prek
run: npm install -g @j178/prek

Expand Down Expand Up @@ -725,7 +747,7 @@ jobs:

BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist")"
VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PATH/Contents/Info.plist")"
BRIDGE_SUMMARY="$(node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); if (m.placeholder !== true) { console.error('::error::Expected thin-app-smoke to bundle the diagnostic Bridge placeholder'); process.exit(1); } console.log('diagnostic-placeholder')" "$BRIDGE_MANIFEST")"
BRIDGE_SUMMARY="$(node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync(process.argv[1], 'utf8')); const sha=/^[0-9a-f]{40}$/i; if (m.placeholder === true) { console.log('diagnostic-placeholder'); process.exit(0); } if (!sha.test(String(m.requestedSourceRef || '')) || !sha.test(String(m.sourceCommit || '')) || m.requestedSourceRef !== m.sourceCommit) { console.error('::error::Expected thin-app-smoke to bundle a diagnostic Bridge placeholder or the pinned real Bridge source'); process.exit(1); } console.log('pinned-real-bridge@' + String(m.sourceCommit).slice(0, 12));" "$BRIDGE_MANIFEST")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare Bridge manifest to the pinned workflow ref

In the thin-app smoke workflow, the build step pins EVAOS_DESKTOP_BRIDGE_SOURCE_REF to a specific commit, but this verification only checks that the manifest's requestedSourceRef and sourceCommit agree with each other. If the packaging code stops honoring the workflow pin and packages a different Bridge commit while writing matching manifest fields, this new acceptance path still passes; compare sourceCommit against the pinned workflow ref instead of trusting the manifest's requested ref.

Useful? React with 👍 / 👎.

node scripts/evaosVerifyRendererDependencyPrune.js "$APP_PATH"

{
Expand All @@ -738,7 +760,7 @@ jobs:
echo "- Bridge manifest: \`$BRIDGE_SUMMARY\`"
echo "- Renderer dependency prune: verified \`app.asar\` and \`app.asar.unpacked/node_modules\`"
echo
echo "This PR artifact is intentionally unpacked and may use a diagnostic Bridge placeholder when private source access is unavailable. It may be ad-hoc/non-Developer-ID signed by packaging hooks. It produces no DMG, no release metadata, no notarization/stapling proof, no TCC or permission proof, and no public release proof."
echo "This PR artifact is intentionally unpacked and may use a diagnostic Bridge placeholder when private source access is unavailable, or a pinned real Bridge source when CI can fetch it. It may be ad-hoc/non-Developer-ID signed by packaging hooks. It produces no DMG, no release metadata, no notarization/stapling proof, no TCC or permission proof, and no public release proof."
echo
echo "### Size"
echo '```'
Expand Down
87 changes: 87 additions & 0 deletions .github/workflows/public-security-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Public Security Scan

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read
pull-requests: read

jobs:
public-sensitive-content:
name: Public Sensitive Content
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
persist-credentials: false

- name: Checkout trusted public scanner
if: github.event_name == 'pull_request'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
ref: ${{ github.base_ref }}
path: trusted-security-base
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: 24

- name: Run repo-owned public scan
shell: bash
run: |
set -euo pipefail
TRUSTED_SCANNER="trusted-security-base/scripts/security/check-public-sensitive-content.js"
if [ "${{ github.event_name }}" = "pull_request" ] && [ -f "$TRUSTED_SCANNER" ]; then
node "$TRUSTED_SCANNER" "$PWD"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Trusted scanner runs against PR checkout, so PR can neuter its own gate

In 'Run repo-owned public scan', when the trusted scanner is present it is executed as node trusted-security-base/... "$PWD". $PWD is the PR-head checkout, and scanRepository enumerates files via git ls-files -z using the PR's own .git index and working tree. The scanner binary comes from the base ref, but the file set and content it inspects are entirely attacker/PR-controlled. A PR that adds, say, a tracked secrets/.env or a real sk-... key to a non-allowlisted path is scanned — good — but a PR can also ADD allowlist entries or change which files are enumerated because it controls the working tree and index. Since the scanner source is from base, allowlist edits in the PR won't take effect (good), but the file enumeration and content are still PR-controlled, so any rule the PR can evade by file placement (e.g., a >8MB file, or a binary that isProbablyTextBuffer skips) bypasses the gate. This is acceptable for a public gate but should be documented; consider scanning git diff of the PR rather than the full tracked set, or at minimum note that >MAX_TEXT_FILE_BYTES and NUL-containing files are skipped silently (no finding emitted), which is a real coverage gap for a secrets gate.

Why this matters: A public secrets gate that silently skips large or binary files can miss committed secrets; the threat model should at least be explicit so reviewers know the gate is best-effort, not complete.

else
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "::warning::Trusted public scanner is missing on ${GITHUB_BASE_REF}; using PR scanner for bootstrap."
fi
node scripts/security/check-public-sensitive-content.js "$PWD"
fi

gitleaks:
name: Gitleaks Full History
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
persist-credentials: false

Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e
Comment on lines +62 to +63

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run Gitleaks with trusted PR configuration

On pull_request, this checkout is the PR merge tree and the Gitleaks action is invoked without GITLEAKS_CONFIG; the action docs say a root gitleaks.toml is automatically detected and used, so a PR can commit a broad allowlist or disabled rules alongside a real npm/Stripe/database secret and make this new security job pass. Mirror the trusted-base pattern used for the repo-owned scanner, or otherwise point Gitleaks at a base-branch config while scanning the PR tree.

Useful? React with 👍 / 👎.

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: false
GITLEAKS_NOTIFY_USER_LIST: ''

trufflehog:
name: TruffleHog Verified
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with:
fetch-depth: 0
persist-credentials: false

- name: Run TruffleHog verified full-history scan
run: |
docker run --rm \
-v "$PWD:/repo:ro" \
ghcr.io/trufflesecurity/trufflehog:3.95.7 \
git file:///repo \
--only-verified \
--no-update \
--fail
34 changes: 34 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
title = "AionUi public repository Gitleaks configuration"

[extend]
useDefault = true

[[allowlists]]
description = "Fake short OpenAI protocol detector fixture"
condition = "AND"
paths = ['''^tests/unit/common/protocolDetector\.test\.ts$''']
regexes = ['''sk-abc123def456ghi789''']

[[allowlists]]
description = "Fake long OpenAI protocol detector fixture"
condition = "AND"
paths = ['''^tests/unit/common/protocolDetector\.test\.ts$''']
regexes = ['''sk-abc123def456ghi789jkl012''']

[[allowlists]]
description = "Fake Google API key protocol detector fixture"
condition = "AND"
paths = ['''^tests/unit/common/protocolDetector\.test\.ts$''']
regexes = ['''AIzaa{35}''']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Gitleaks allowlist regex AIzaa{35} over-matches; will not match the 35-a fixture and may leak/err

In the 'Fake Google API key protocol detector fixture' allowlist, regexes = ['''AIzaa{35}'''] is interpreted by Gitleaks as a Go regexp: AIza + one literal a + the quantifier {35}, i.e. it matches AIza followed by 36 a's. The test fixture it is meant to allow (tests/unit/common/protocolDetector.test.ts) builds the key as 'AIza' + 'a'.repeat(35) = AIza + 35 a's (39 chars total). The allowlist therefore does NOT match the fixture it claims to allow. Either the Gitleaks job will flag the legitimate test fixture (CI-breaking false positive) or, worse, the allowlist silently matches nothing and the rule provides no real coverage. The in-repo custom scanner uses a correctly escaped pattern (/'''AIzaa\{35\}'''/, line 118 of check-public-sensitive-content.js), confirming intent was a literal. Fix: align .gitleaks.toml with the literal the fixture produces, e.g. regexes = ['''AIzaaaa{34}'''] won't help; use the explicit literal AIza + 35 a's, or drop the meta-quantifier.

Why this matters: A misconfigured allowlist either breaks the new required CI gate (Gitleaks Full History) with false positives on a known fixture, or silently disables coverage so a real Google key could pass review.


[[allowlists]]
description = "Fake bridge resource token fixture"
condition = "AND"
paths = ['''^tests/unit/bootstrap/prepareEvaosDesktopBridgeResource\.test\.ts$''']
regexes = ['''super-secret-token''']

[[allowlists]]
description = "Fake bridge resource credential URL fixture"
condition = "AND"
paths = ['''^tests/unit/bootstrap/prepareEvaosDesktopBridgeResource\.test\.ts$''']
regexes = ['''https://x-access-token:super-secret-token@github\.com/electricsheephq/evaos-desktop-bridge\.git''']
12 changes: 8 additions & 4 deletions mobile/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,18 @@
"submit": {
"production": {
"ios": {
"appleId": "liangzhewei@gmail.com",
"appleTeamId": "M4AG47ZV62"
"appName": "AionUi Mobile",
"bundleIdentifier": "ai.resopod.aionui",
"language": "en-US",
"sku": "aionui-mobile"
}
},
"preview": {
"ios": {
"appleId": "liangzhewei@gmail.com",
"appleTeamId": "M4AG47ZV62"
"appName": "AionUi Mobile",
"bundleIdentifier": "ai.resopod.aionui",
"language": "en-US",
"sku": "aionui-mobile-preview"
}
}
}
Expand Down
70 changes: 56 additions & 14 deletions mobile/scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const projectRoot = path.join(__dirname, '..');

// Parse command line arguments
const args = process.argv.slice(2);
const profileIndex = args.indexOf('--profile');
Expand All @@ -28,6 +30,31 @@ if (!profile) {
process.exit(1);
}

function requireEnv(name, purpose) {
const value = process.env[name];
if (!value) {
console.error(`Error: ${name} is required for ${purpose}.`);
process.exit(1);
}
return value;
}

const expoAppleId = process.env.EXPO_APPLE_ID || process.env.APPLE_ID || process.env.appleId;
const expoAppleTeamId = process.env.EXPO_APPLE_TEAM_ID || process.env.TEAM_ID || process.env.teamId;

if (!isLocal && directSubmit) {
console.error('Error: --direct-submit is only supported with --local builds.');
process.exit(1);
}

if (platform === 'ios' && (autoSubmit || directSubmit) && !expoAppleId) {
requireEnv('EXPO_APPLE_ID', 'iOS submission');
}

if (platform === 'ios' && autoSubmit && !expoAppleTeamId) {
requireEnv('EXPO_APPLE_TEAM_ID', 'EAS iOS submission');
}

// Read current version
const versionPath = path.join(__dirname, '..', 'versions', 'version.json');
let versionData;
Expand All @@ -53,9 +80,13 @@ try {

// Build eas command args
const outputExt = platform === 'ios' ? '.ipa' : '.apk';
const localOutputPath = path.join(__dirname, '..', `build-${Date.now()}${outputExt}`);
const localOutputPath = path.join(projectRoot, `build-${Date.now()}${outputExt}`);
let buildArgs = args.filter((a) => a !== '--auto-submit' && a !== '--direct-submit');

if (!isLocal && autoSubmit && !buildArgs.includes('--auto-submit-with-profile')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Non-local auto-submit rewrites --auto-submit to --auto-submit-with-profile but eas submit profiles no longer carry Apple credentials

For cloud (!isLocal) builds with --auto-submit, the script strips --auto-submit and pushes --auto-submit-with-profile <profile>, then relies on EXPO_APPLE_ID/EXPO_APPLE_TEAM_ID being set in appleEnv (validated above). That part is consistent. However, the EAS submit profiles in mobile/eas.json (production/preview) were stripped of appleId/appleTeamId in this same PR, so EAS submit now depends entirely on these env vars being present at submit time. The validation at lines 50-56 covers iOS, but if a caller runs --auto-submit with --platform android (or no platform defaulting to ios) the env checks are skipped for android — which is correct, but there is no assertion that the submit profile actually exists for the platform. This is low risk (EAS will error clearly) but the coupling between the removed eas.json fields and the new env-var contract is undocumented; a maintainer running cloud iOS auto-submit without the env vars set will get a failure inside EAS rather than the script's clear requireEnv message, because requireEnv only runs for autoSubmit when !expoAppleTeamId — and a partial set (id present, team id missing) is handled, but the reverse ordering is fine. Net: behavior is correct, but consider adding a one-line comment in eas.json or build.js documenting that Apple credentials are now env-only.

Why this matters: The implicit contract between eas.json (credentials removed) and build.js (env vars now mandatory) is easy to break in a future edit; a comment prevents regression.

buildArgs.push('--auto-submit-with-profile', profile);
}

// For local builds with submit, capture output path for later submission
if (isLocal && (autoSubmit || directSubmit)) {
if (!buildArgs.includes('--output')) {
Expand Down Expand Up @@ -83,21 +114,31 @@ if (isLocal && platform === 'ios') {
// Build the eas command
const easCommand = `eas build ${buildArgs.join(' ')}`;
console.log(`\nRunning: ${easCommand}\n`);

// Apple-specific env vars (only needed for iOS builds)
const appleEnv =
platform === 'ios'
? {
EXPO_APPLE_TEAM_ID: process.env.EXPO_APPLE_TEAM_ID || 'M4AG47ZV62',
EXPO_APPLE_ID: process.env.EXPO_APPLE_ID || 'liangzhewei@gmail.com',
...(applePassword ? { EXPO_APPLE_PASSWORD: applePassword } : {}),
}
: {};
const appleEnv = {};
if (platform === 'ios') {
if (expoAppleTeamId) {
appleEnv.EXPO_APPLE_TEAM_ID = expoAppleTeamId;
}
if (expoAppleId) {
appleEnv.EXPO_APPLE_ID = expoAppleId;
}
if (applePassword) {
appleEnv.EXPO_APPLE_PASSWORD = applePassword;
}
if (autoSubmit || directSubmit) {
appleEnv.EXPO_APPLE_ID = expoAppleId || requireEnv('EXPO_APPLE_ID', 'iOS submission');
}
if (autoSubmit) {
appleEnv.EXPO_APPLE_TEAM_ID =
expoAppleTeamId || requireEnv('EXPO_APPLE_TEAM_ID', 'EAS iOS submission');
Comment thread
100yenadmin marked this conversation as resolved.
}
}

// Execute eas build
try {
execSync(easCommand, {
stdio: 'inherit',
cwd: projectRoot,
env: {
...process.env,
PATH: `/usr/bin:${process.env.PATH}`,
Expand All @@ -123,7 +164,7 @@ if (platform === 'ios' && isLocal && (autoSubmit || directSubmit)) {

if (directSubmit) {
// Upload directly to App Store Connect via xcrun altool (bypasses EAS)
const appleId = process.env.APPLE_ID || 'liangzhewei@gmail.com';
const appleId = expoAppleId || requireEnv('EXPO_APPLE_ID', 'direct iOS submission');
const submitCommand = `xcrun altool --upload-app -f "${outputFile}" -t ${platform} -u "${appleId}" -p "@keychain:AC_PASSWORD"`;
console.log(`\nUploading directly to TestFlight: xcrun altool --upload-app\n`);
try {
Expand All @@ -135,17 +176,18 @@ if (platform === 'ios' && isLocal && (autoSubmit || directSubmit)) {
' Make sure your App-Specific Password is saved in Keychain as "AC_PASSWORD".',
);
console.error(
' To save it: security add-generic-password -a "liangzhewei@gmail.com" -s "AC_PASSWORD" -w "<your-app-specific-password>" -U',
' To save it: security add-generic-password -a "$EXPO_APPLE_ID" -s "AC_PASSWORD" -w "<your-app-specific-password>" -U',
);
process.exit(1);
}
} else {
// Upload via EAS submit
const submitCommand = `eas submit --platform ${platform} --path ${outputFile} --non-interactive`;
const submitCommand = `eas submit --platform ${platform} --profile ${profile} --path ${outputFile} --non-interactive`;
console.log(`\nSubmitting to TestFlight: ${submitCommand}\n`);
try {
execSync(submitCommand, {
stdio: 'inherit',
cwd: projectRoot,
env: {
...process.env,
...appleEnv,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"lint:fix": "oxlint --fix",
"format": "oxfmt",
"format:check": "oxfmt --check",
"security:public-scan": "node scripts/security/check-public-sensitive-content.js",
"i18n:types": "node scripts/generate-i18n-types.js",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
Loading
Loading