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
12 changes: 12 additions & 0 deletions src/pipeline/stellar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { analyseSorobanContract } from "./soroban-pipeline";
export type { SorobanRuleContext } from "./types";

export {
MissingAccessControlRule,
WeakRoleHierarchyRule,
} from "./rules/access-control.rule";
export { MissingUpgradeGuardRule } from "./rules/upgradeability.rule";
export { UnsafeCrossContractRule } from "./rules/cross-contract.rule";
export { InefficientSymbolUsageRule } from "./rules/optimization.rule";
export { ExcessiveEventTopicsRule } from "./rules/events.rule";
export { InconsistentVisibilityRule } from "./rules/visibility.rule";
52 changes: 52 additions & 0 deletions src/pipeline/stellar/rules/access-control.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectMissingAccessControl } from "../../../../rules/stellar/access-control/detect-missing-access-control";
import { detectWeakRoleHierarchies } from "../../../../rules/stellar/access-control/detect-weak-role-hierarchies";

export class MissingAccessControlRule extends BaseRule {
id = "soroban/missing-access-control";
name = "Missing Access Control";
description = "Flags privileged functions that lack role validation.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectMissingAccessControl(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"missing-access-control",
"high",
result.message,
result.suggestion,
filePath,
{ flaggedFunctions: result.flaggedFunctions },
),
];
}
}

export class WeakRoleHierarchyRule extends BaseRule {
id = "soroban/weak-role-hierarchy";
name = "Weak Role Hierarchy";
description =
"Flags role-assignment functions without a superior-authority check.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectWeakRoleHierarchies(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"weak-role-hierarchy",
"high",
result.message,
result.suggestion,
filePath,
{ weakRoles: result.weakRoles },
),
];
}
}
27 changes: 27 additions & 0 deletions src/pipeline/stellar/rules/cross-contract.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectUnsafeCrossContractInvocation } from "../../../../rules/stellar/cross-contract/detect-unsafe-cross-contract-invocation";

export class UnsafeCrossContractRule extends BaseRule {
id = "soroban/unsafe-cross-contract";
name = "Unsafe Cross-Contract Invocation";
description =
"Flags cross-contract calls without caller validation or result checks.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectUnsafeCrossContractInvocation(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"unsafe-cross-contract",
"high",
result.message,
result.suggestion,
filePath,
),
];
}
}
28 changes: 28 additions & 0 deletions src/pipeline/stellar/rules/events.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectExcessiveEventTopics } from "../../../../rules/stellar/events/detect-excessive-event-topics";

export class ExcessiveEventTopicsRule extends BaseRule {
id = "soroban/excessive-event-topics";
name = "Excessive Event Topics";
description =
"Flags events with too many topics or oversized topic payloads.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectExcessiveEventTopics(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"excessive-event-topics",
"warning",
result.message,
result.suggestion,
filePath,
{ violations: result.violations },
),
];
}
}
28 changes: 28 additions & 0 deletions src/pipeline/stellar/rules/optimization.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectInefficientSymbolUsage } from "../../../../rules/stellar/optimization/detect-inefficient-symbol-usage";

export class InefficientSymbolUsageRule extends BaseRule {
id = "soroban/inefficient-symbol-usage";
name = "Inefficient Symbol Usage";
description =
"Flags repeated inline Symbol construction that should use static references.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectInefficientSymbolUsage(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"inefficient-symbol-usage",
"warning",
result.message,
result.suggestion,
filePath,
{ repeated: result.repeated },
),
];
}
}
31 changes: 31 additions & 0 deletions src/pipeline/stellar/rules/upgradeability.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectMissingUpgradeGuards } from "../../../../rules/stellar/upgradeability/detect-missing-upgrade-guards";

export class MissingUpgradeGuardRule extends BaseRule {
id = "soroban/missing-upgrade-guard";
name = "Missing Upgrade Guard";
description = "Flags upgrade functions that lack admin authorisation.";

getDependencies(): string[] {
// Access-control scan must run first so its output can enrich context downstream
return ["soroban/missing-access-control"];
}

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectMissingUpgradeGuards(source);
if (!result.detected) return [];
return [
toViolation(
this.id,
"missing-upgrade-guard",
"critical",
result.message,
result.suggestion,
filePath,
),
];
}
}
28 changes: 28 additions & 0 deletions src/pipeline/stellar/rules/visibility.rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseRule } from "../../../analysis/pipeline/base-rule";
import { RuleContext, RuleViolation } from "../../../analysis/pipeline/types";
import { SorobanRuleContext, toViolation } from "../types";
import { detectInconsistentVisibility } from "../../../../rules/stellar/security/visibility/detect-inconsistent-visibility";

export class InconsistentVisibilityRule extends BaseRule {
id = "soroban/inconsistent-visibility";
name = "Inconsistent Function Visibility";
description =
"Flags pub fn declarations inside #[contractimpl] that should be private.";

async analyze(context: RuleContext): Promise<RuleViolation[]> {
const { source, filePath } = context as SorobanRuleContext;
const result = detectInconsistentVisibility(source);
if (!result.detected) return [];
return result.violations.map((v) =>
toViolation(
this.id,
`visibility/${v.kind}`,
"medium",
`${v.functionName}: ${v.reason}`,
result.suggestion,
filePath,
{ functionName: v.functionName, line: v.line, kind: v.kind },
),
);
}
}
55 changes: 55 additions & 0 deletions src/pipeline/stellar/soroban-pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { PipelineExecutor } from "../../analysis/pipeline/pipeline-executor";
import { ExecutionResult } from "../../analysis/pipeline/types";
import { SorobanRuleContext } from "./types";

import {
MissingAccessControlRule,
WeakRoleHierarchyRule,
} from "./rules/access-control.rule";
import { MissingUpgradeGuardRule } from "./rules/upgradeability.rule";
import { UnsafeCrossContractRule } from "./rules/cross-contract.rule";
import { InefficientSymbolUsageRule } from "./rules/optimization.rule";
import { ExcessiveEventTopicsRule } from "./rules/events.rule";
import { InconsistentVisibilityRule } from "./rules/visibility.rule";

/** Build a PipelineExecutor pre-loaded with all Soroban rules. */
function createSorobanPipeline(): PipelineExecutor {
const executor = new PipelineExecutor();

executor.registerRules([
// Stage 1 – independent checks (no deps)
new MissingAccessControlRule(),
new WeakRoleHierarchyRule(),
new UnsafeCrossContractRule(),
new InefficientSymbolUsageRule(),
new ExcessiveEventTopicsRule(),
new InconsistentVisibilityRule(),
// Stage 2 – depends on access-control result
new MissingUpgradeGuardRule(),
]);

return executor;
}

/**
* Analyse a single Soroban contract source file through the standardised pipeline.
*
* @param source Raw contract source code
* @param filePath Path used for violation location metadata
* @param config Optional rule configuration forwarded to the context
*/
export async function analyseSorobanContract(
source: string,
filePath: string,
config?: Record<string, unknown>,
): Promise<ExecutionResult> {
const executor = createSorobanPipeline();

const context: Omit<SorobanRuleContext, "priorResults"> = {
source,
filePath,
config,
};

return executor.execute(context);
}
28 changes: 28 additions & 0 deletions src/pipeline/stellar/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RuleContext, RuleViolation } from "../../analysis/pipeline/types";

/** Shared context shape for all Soroban pipeline rules */
export interface SorobanRuleContext extends RuleContext {
/** Raw Soroban contract source */
source: string;
/** File path of the contract being analysed */
filePath: string;
}

export function toViolation(
ruleId: string,
type: string,
severity: RuleViolation["severity"],
message: string,
suggestion: string,
filePath: string,
metadata?: Record<string, unknown>,
): RuleViolation {
return {
ruleId,
type,
severity,
message,
location: { file: filePath },
metadata: suggestion ? { ...metadata, suggestion } : metadata,
};
}
Loading