Skip to content
Open
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
18 changes: 17 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,23 @@ jobs:

- run: npm install -g npm@latest

- run: npm publish --access public --provenance
- name: Verify version sync
run: node scripts/version.mjs --check

- name: Publish @guard0/g0
run: npm publish --access public --provenance

- name: Publish guard0 wrapper
working-directory: packages/guard0
run: |
LOCAL_VERSION=$(node -p "require('./package.json').version")
REMOTE_VERSION=$(npm view guard0 version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" = "$REMOTE_VERSION" ]; then
echo "guard0 wrapper v${LOCAL_VERSION} already published — skipping"
else
echo "Publishing guard0 wrapper v${LOCAL_VERSION} (registry has v${REMOTE_VERSION})"
npm publish --access public --provenance
fi

- name: Publish OpenClaw plugin
working-directory: packages/g0-openclaw-plugin
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ GAPS.md
docs/openclaw-plugin-validation.md
docs/v1.5.0-validation-runbook.md
docs/v1.5.0-customer-brief.md
docs/EVALUATION.md
docs/ARCHITECTURE_REVIEW.md
tests/integration/openclaw-plugin-hooks.test.mjs
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"lint": "tsc --noEmit",
"typecheck": "tsc --noEmit",
"g0": "tsx bin/g0.ts",
"prepublishOnly": "tsup && vitest run"
"version:check": "node scripts/version.mjs --check",
"version:bump": "node scripts/version.mjs",
"prepublishOnly": "node scripts/version.mjs --check && tsup && vitest run"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -92,7 +94,7 @@
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.1.0",
"handlebars": "^4.7.8",
"handlebars": "^4.7.8",
"ignore": "^6.0.2",
"ora": "^8.1.0",
"yaml": "^2.5.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/guard0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<p align="center">
<img src="https://raw.githubusercontent.com/guard0-ai/g0/main/assets/logo.png" alt="Guard0" width="200">
</p>

<h1 align="center">Guard0 — The Control Layer for AI Agents</h1>

<p align="center">
<a href="https://www.npmjs.com/package/@guard0/g0"><img src="https://img.shields.io/npm/v/@guard0/g0.svg" alt="npm version"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg" alt="Node.js >= 20"></a>
<a href="https://owasp.org/www-project-agentic-security/"><img src="https://img.shields.io/badge/OWASP-Agentic%20Top%2010-orange.svg" alt="OWASP Agentic"></a>
</p>

This is the convenience package for **[Guard0](https://guard0.ai)** (`@guard0/g0`). It lets you install Guard0 without the scoped package name.

## Install

```bash
npm install -g guard0
```

## Usage

```bash
g0 scan ./my-agent # Security assessment
g0 scan https://github.com/org/repo # Remote repo scan
g0 inventory . # AI Bill of Materials
g0 test --target http://localhost:3000/api/chat # Adversarial testing
g0 endpoint # Developer machine assessment
g0 detect # AI tool & MDM detection
```

## Documentation

See the full documentation at **[@guard0/g0](https://github.com/guard0-ai/g0)**.
49 changes: 49 additions & 0 deletions packages/guard0/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "guard0",
"version": "1.7.2",
"description": "The control layer for AI agents — discover, assess, and govern your agent infrastructure",
"dependencies": {
"@guard0/g0": "1.7.2"
},
"bin": {
"g0": "./node_modules/@guard0/g0/dist/bin/g0.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/guard0-ai/g0.git",
"directory": "packages/guard0"
},
"homepage": "https://guard0.ai",
"bugs": {
"url": "https://github.com/guard0-ai/g0/issues"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"ai",
"agent",
"security",
"guard0",
"ai-security",
"agent-security",
"mcp",
"mcp-security",
"owasp",
"owasp-agentic",
"agentic",
"sast",
"dast",
"adversarial-testing",
"control-layer",
"ai-governance",
"compliance",
"langchain",
"crewai",
"openai",
"vercel-ai",
"llm-security"
],
"author": "Rakshan AI Inc. (dba Guard0)",
"license": "AGPL-3.0-or-later"
}
137 changes: 137 additions & 0 deletions scripts/version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env node

/**
* Version management script for guard0 monorepo.
*
* Usage:
* node scripts/version.mjs <new-version>
* node scripts/version.mjs patch|minor|major
* node scripts/version.mjs --check # verify all packages are in sync
*
* Updates version across:
* - package.json (@guard0/g0 — main package)
* - packages/guard0/ (guard0 — wrapper / vanity package)
*
* The wrapper's @guard0/g0 dependency is pinned to the exact version (no caret)
* so `npm install guard0@1.8.0` always resolves to `@guard0/g0@1.8.0`.
*/

import { readFileSync, writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');

const PACKAGES = [
{ path: 'package.json', label: '@guard0/g0' },
{ path: 'packages/guard0/package.json', label: 'guard0 (wrapper)' },
];

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

function readPkg(rel) {
const abs = resolve(root, rel);
return JSON.parse(readFileSync(abs, 'utf-8'));
}

function writePkg(rel, obj) {
const abs = resolve(root, rel);
writeFileSync(abs, JSON.stringify(obj, null, 2) + '\n');
}

function bumpVersion(current, level) {
const [major, minor, patch] = current.split('.').map(Number);
switch (level) {
case 'major': return `${major + 1}.0.0`;
case 'minor': return `${major}.${minor + 1}.0`;
case 'patch': return `${major}.${minor}.${patch + 1}`;
default: throw new Error(`Unknown bump level: ${level}`);
}
}

function isValidSemver(v) {
return /^\d+\.\d+\.\d+(-[\w.]+)?$/.test(v);
}

// ---------------------------------------------------------------------------
// --check mode
// ---------------------------------------------------------------------------

function checkSync() {
const mainPkg = readPkg('package.json');
const wrapperPkg = readPkg('packages/guard0/package.json');
const mainVersion = mainPkg.version;
const wrapperVersion = wrapperPkg.version;
const wrapperDep = wrapperPkg.dependencies?.['@guard0/g0'];

let ok = true;

if (wrapperVersion !== mainVersion) {
console.error(` MISMATCH guard0 wrapper is ${wrapperVersion}, main is ${mainVersion}`);
ok = false;
}
if (wrapperDep !== mainVersion) {
console.error(` MISMATCH guard0 wrapper depends on @guard0/g0@${wrapperDep}, main is ${mainVersion}`);
ok = false;
}

if (ok) {
console.log(` OK all packages at v${mainVersion}`);
} else {
console.error('\nRun: node scripts/version.mjs <version> to fix');
process.exit(1);
}
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------

const arg = process.argv[2];

if (!arg) {
console.error('Usage: node scripts/version.mjs <version|patch|minor|major|--check>');
process.exit(1);
}

if (arg === '--check') {
checkSync();
process.exit(0);
}

const mainPkg = readPkg('package.json');
const currentVersion = mainPkg.version;

const newVersion = ['patch', 'minor', 'major'].includes(arg)
? bumpVersion(currentVersion, arg)
: arg;

if (!isValidSemver(newVersion)) {
console.error(`Invalid version: ${newVersion}`);
process.exit(1);
}

if (newVersion === currentVersion) {
console.log(`Already at v${currentVersion} — nothing to do`);
process.exit(0);
}

console.log(`\n ${currentVersion} → ${newVersion}\n`);

// 1. Main package
mainPkg.version = newVersion;
writePkg('package.json', mainPkg);
console.log(` updated @guard0/g0 → ${newVersion}`);

// 2. Wrapper package — version + pinned dependency
const wrapperPkg = readPkg('packages/guard0/package.json');
wrapperPkg.version = newVersion;
wrapperPkg.dependencies['@guard0/g0'] = newVersion;
writePkg('packages/guard0/package.json', wrapperPkg);
console.log(` updated guard0 (wrapper) → ${newVersion}`);
console.log(` pinned guard0 → @guard0/g0@${newVersion}`);

console.log(`\n Done. All packages at v${newVersion}\n`);
51 changes: 45 additions & 6 deletions src/analyzers/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const TEST_FILE_PATTERNS = [
/\/examples?\//, /\/docs?\//, /\/tutorials?\//, /\/notebooks?\//,
/\/demo\//, /\/samples?\//, /\/quickstart\//, /\/cookbook\//,
/\/benchmarks?\//, /\/e2e\//, /\/integration_tests?\//,
/\/\.claude\/worktrees\//, /\/advisories?\//, /\/cve[s-]?\//i,
];

const TEST_SEVERITY_DOWNGRADE: Record<string, Severity> = {
Expand Down Expand Up @@ -94,15 +95,13 @@ export function runAnalysis(graph: AgentGraph, options?: AnalysisOptions): Findi

for (const rule of rules) {
// For project_missing rules, skip if the control registry shows the control exists
const ruleRecord = rule as Rule & Record<string, unknown>;
if (ruleRecord.requiresControl && registry) {
if (registry.hasControl(ruleRecord.requiresControl as SecurityControlType)) continue;
if (rule.requiresControl && registry) {
if (registry.hasControl(rule.requiresControl)) continue;
}

// For rules with suppressed_by, skip if ALL listed controls are present
if (ruleRecord.suppressedBy && registry) {
const suppressors = ruleRecord.suppressedBy as string[];
const allPresent = suppressors.every((s: string) => registry.hasControl(s as SecurityControlType));
if (rule.suppressedBy && registry) {
const allPresent = rule.suppressedBy.every(s => registry.hasControl(s));
if (allPresent) continue;
}

Expand Down Expand Up @@ -191,6 +190,46 @@ export function runAnalysis(graph: AgentGraph, options?: AnalysisOptions): Findi
result = result.filter(f => SEVERITY_ORDER[f.severity] <= minLevel);
}

// Non-agent project guard: if no agents or tools detected, drop hardening findings
if (graph.agents.length === 0 && graph.tools.length === 0) {
result = result.filter(f => {
// Keep findings that are already categorized as vulnerability
if (f.category === 'vulnerability') return true;
// For uncategorized findings, use title heuristic
const t = f.title.toLowerCase();
const isAbsence = t.startsWith('no ') || t.startsWith('missing ') || t.startsWith('lacks ');
return !isAbsence;
});
}

// Derive finding category (vulnerability vs hardening vs informational)
const absenceCheckTypes = new Set([
'prompt_missing', 'tool_missing_property', 'project_missing', 'absence_check',
]);
for (const f of result) {
if (f.category) continue; // already set by rule
if (f.severity === 'info') {
f.category = 'informational';
} else if (f.checkType && absenceCheckTypes.has(f.checkType)) {
f.category = 'hardening';
} else if (f.checkType === 'agent_property') {
const t = f.title.toLowerCase();
if (t.startsWith('no ') || t.includes('missing') || t.includes('lacks') || t.includes('without')) {
f.category = 'hardening';
} else {
f.category = 'vulnerability';
}
} else {
// Default: TS rules with absence-indicating titles
const t = f.title.toLowerCase();
if (t.startsWith('no ') || t.startsWith('missing ') || t.startsWith('lacks ')) {
f.category = 'hardening';
} else {
f.category = 'vulnerability';
}
}
}

return result;
}

Expand Down
8 changes: 8 additions & 0 deletions src/analyzers/parsers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ export function checkInstructionGuarding(prompt: string): boolean {
/do\s+not\s+reveal\s+your\s+(instructions|prompt|system)/i,
/instruction\s+(boundary|boundaries)/i,
/prompt\s+injection\s+(protect|guard|prevent|detect)/i,
// Broader explicit deny patterns — common in well-written system prompts
/\bMUST\s+NOT\b/,
/\byou\s+(can\s+)?only\b/i,
/\b(do\s+not|don'?t)\s+(disclose|reveal|share|access|execute|modify)/i,
/\b(outside|beyond)\s+(these|those|the)\s+(boundaries|limits|scope|constraints)/i,
/\bpolitely\s+(decline|refuse|reject)/i,
/\bif\s+asked\s+to\s+(do\s+)?anything\s+(outside|beyond|other)/i,
/\brefuse\s+(any|all)\s+(requests?|instructions?)\s+(that|which|to)/i,
];
return guards.some(g => g.test(prompt));
}
Expand Down
10 changes: 7 additions & 3 deletions src/analyzers/rules/data-leakage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1761,11 +1761,15 @@ export const dataLeakageRules: Rule[] = [
for (const file of [...graph.files.python, ...graph.files.typescript, ...graph.files.javascript]) {
let content: string;
try { content = fs.readFileSync(file.path, 'utf-8'); } catch { continue; }
const pattern = /(?:ConversationBufferMemory|ConversationSummaryMemory|ChatMessageHistory|InMemoryChatMessageHistory|MemorySaver|memory\s*=\s*\{)/gi;
const memPattern = /(?:ConversationBufferMemory|ConversationSummaryMemory|ChatMessageHistory|InMemoryChatMessageHistory|MemorySaver|memory\s*=\s*\{)/gi;
let match: RegExpExecArray | null;
while ((match = pattern.exec(content)) !== null) {
while ((match = memPattern.exec(content)) !== null) {
// Skip import/require statements — only flag actual usage
const lineStart = content.lastIndexOf('\n', match.index) + 1;
const lineText = content.substring(lineStart, content.indexOf('\n', match.index));
if (/^\s*(from\s+|import\s+|const\s+.*require)/.test(lineText)) continue;
const region = content.substring(Math.max(0, match.index - 400), Math.min(content.length, match.index + 400));
if (!/user[_.]?id|tenant[_.]?id|session[_.]?id|per.?user|isolat|partition|namespace.*user/i.test(region)) {
if (!/user[_.]?id|tenant[_.]?id|session[_.]?id|per.?user|isolat|partition|namespace.*user|thread_id|config.*thread|configurable|memory_key\s*=|chat_history/i.test(region)) {
const line = content.substring(0, match.index).split('\n').length;
findings.push({
id: `AA-DL-046-${findings.length}`, ruleId: 'AA-DL-046',
Expand Down
Loading
Loading