mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
7cdb4ab7e2
* docs: add agent collaboration and git safety rules to AGENTS.md
* fix(portability): harden cross-platform path handling and install workflows
- add shared path resolution utility for advisory guardian components
- expand and normalize home-path tokens: ~, $HOME, ${HOME}, %USERPROFILE%, $env:USERPROFILE
- reject unresolved/escaped home tokens to prevent literal "$HOME" directory creation
- fix install/runtime path handling in:
- openclaw-audit-watchdog setup_cron and suppression config loader
- clawsec-suite advisory hook handler, suppression loader, and guarded installer
- remove hardcoded Homebrew binary assumptions in watchdog scripts/tests
- add LF enforcement via .gitattributes to reduce CRLF script breakage
- expand CI Node checks to linux/macos/windows matrix
- add cross-platform test coverage for path expansion and token rejection
- update README and SKILL docs with bash/zsh/PowerShell-safe path guidance
- add compatibility deliverables:
- docs/COMPATIBILITY_REPORT.md
- docs/REMEDIATION_PLAN.md
- docs/PLATFORM_VERIFICATION.md
Validation:
- node skills/clawsec-suite/test/path_resolution.test.mjs
- node skills/clawsec-suite/test/guarded_install.test.mjs
- node skills/clawsec-suite/test/advisory_suppression.test.mjs
- node skills/openclaw-audit-watchdog/test/suppression_config.test.mjs
- node skills/openclaw-audit-watchdog/test/render_report_suppression.test.mjs
* fix(advisory): avoid fail-open on invalid path vars and cover watchdog tests
* docs: move signing runbooks into docs folder
* docs: remove root-level signing runbooks after move
* chore(clawsec-suite): bump version to 0.1.3
* chore(openclaw-audit-watchdog): bump version to 0.1.1
* docs(changelog): add entries for clawsec-suite 0.1.3 and watchdog 0.1.1
* docs(changelog): credit @aldodelgado for PR #62 contributions
* feat(clawsec-suite): scope advisories to openclaw application
* fix(ci): run advisory scope tests without TypeScript loader
---------
Co-authored-by: David Abutbul <David.a@prompt.security>
136 lines
3.6 KiB
JavaScript
136 lines
3.6 KiB
JavaScript
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* @param {unknown} value
|
|
* @returns {value is Record<string, unknown>}
|
|
*/
|
|
export function isObject(value) {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
export function normalizeSkillName(value) {
|
|
return String(value ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* @param {string[]} values
|
|
* @returns {string[]}
|
|
*/
|
|
export function uniqueStrings(values) {
|
|
return Array.from(new Set(values));
|
|
}
|
|
|
|
function detectHomeDirectory(env = process.env) {
|
|
if (typeof env.HOME === "string" && env.HOME.trim()) return env.HOME.trim();
|
|
if (typeof env.USERPROFILE === "string" && env.USERPROFILE.trim()) return env.USERPROFILE.trim();
|
|
if (
|
|
typeof env.HOMEDRIVE === "string" &&
|
|
env.HOMEDRIVE.trim() &&
|
|
typeof env.HOMEPATH === "string" &&
|
|
env.HOMEPATH.trim()
|
|
) {
|
|
return `${env.HOMEDRIVE.trim()}${env.HOMEPATH.trim()}`;
|
|
}
|
|
return os.homedir();
|
|
}
|
|
|
|
const UNEXPANDED_HOME_TOKEN_PATTERN =
|
|
/(?:^|[\\/])(?:\\?\$HOME|\\?\$\{HOME\}|\\?\$USERPROFILE|\\?\$\{USERPROFILE\}|%HOME%|%USERPROFILE%|\$env:HOME|\$env:USERPROFILE)(?:$|[\\/])/i;
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {string}
|
|
*/
|
|
function expandKnownHomeTokens(value) {
|
|
const homeDir = detectHomeDirectory(process.env);
|
|
if (!homeDir) return value;
|
|
|
|
let expanded = String(value ?? "");
|
|
|
|
if (expanded === "~") {
|
|
expanded = homeDir;
|
|
} else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
|
|
expanded = path.join(homeDir, expanded.slice(2));
|
|
}
|
|
|
|
expanded = expanded
|
|
.replace(/(?<!\\)\$\{HOME\}/g, homeDir)
|
|
.replace(/(?<!\\)\$HOME(?=$|[\\/])/g, homeDir)
|
|
.replace(/(?<!\\)\$\{USERPROFILE\}/gi, homeDir)
|
|
.replace(/(?<!\\)\$USERPROFILE(?=$|[\\/])/gi, homeDir)
|
|
.replace(/%HOME%/gi, homeDir)
|
|
.replace(/%USERPROFILE%/gi, homeDir)
|
|
.replace(/(?<!\\)\$env:HOME/gi, homeDir)
|
|
.replace(/(?<!\\)\$env:USERPROFILE/gi, homeDir);
|
|
|
|
return expanded;
|
|
}
|
|
|
|
/**
|
|
* @param {string} value
|
|
* @returns {boolean}
|
|
*/
|
|
export function hasUnexpandedHomeToken(value) {
|
|
return UNEXPANDED_HOME_TOKEN_PATTERN.test(String(value ?? "").trim());
|
|
}
|
|
|
|
/**
|
|
* Expand `~` and known home env var patterns in user-provided path-like strings.
|
|
* Also fails fast when unresolved home tokens remain.
|
|
*
|
|
* @param {string} inputPath
|
|
* @param {{label?: string}} [options]
|
|
* @returns {string}
|
|
*/
|
|
export function resolveUserPath(inputPath, { label = "path" } = {}) {
|
|
const raw = String(inputPath ?? "").trim();
|
|
if (!raw) return raw;
|
|
|
|
const expanded = expandKnownHomeTokens(raw);
|
|
const normalized = path.normalize(expanded);
|
|
|
|
if (hasUnexpandedHomeToken(normalized)) {
|
|
throw new Error(
|
|
`Unexpanded home token detected in ${label}: ${raw}. ` +
|
|
"Use an absolute path or an unquoted home-path expression.",
|
|
);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Resolve an optional explicit path; if invalid, fall back to a default path.
|
|
*
|
|
* @param {string | undefined} explicitPath
|
|
* @param {string} fallbackPath
|
|
* @param {{label?: string, onInvalid?: (error: unknown, rawValue: string) => void}} [options]
|
|
* @returns {string}
|
|
*/
|
|
export function resolveConfiguredPath(
|
|
explicitPath,
|
|
fallbackPath,
|
|
{ label = "path", onInvalid } = {},
|
|
) {
|
|
const explicit = typeof explicitPath === "string" ? explicitPath.trim() : "";
|
|
if (!explicit) {
|
|
return resolveUserPath(fallbackPath, { label });
|
|
}
|
|
|
|
try {
|
|
return resolveUserPath(explicit, { label });
|
|
} catch (error) {
|
|
if (typeof onInvalid === "function") {
|
|
onInvalid(error, explicit);
|
|
}
|
|
return resolveUserPath(fallbackPath, { label });
|
|
}
|
|
}
|