diff --git a/src/pipeline/stellar/index.ts b/src/pipeline/stellar/index.ts new file mode 100644 index 0000000..374edf1 --- /dev/null +++ b/src/pipeline/stellar/index.ts @@ -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"; diff --git a/src/pipeline/stellar/rules/access-control.rule.ts b/src/pipeline/stellar/rules/access-control.rule.ts new file mode 100644 index 0000000..6e8b4b3 --- /dev/null +++ b/src/pipeline/stellar/rules/access-control.rule.ts @@ -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 { + 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 { + 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 }, + ), + ]; + } +} diff --git a/src/pipeline/stellar/rules/cross-contract.rule.ts b/src/pipeline/stellar/rules/cross-contract.rule.ts new file mode 100644 index 0000000..36d697a --- /dev/null +++ b/src/pipeline/stellar/rules/cross-contract.rule.ts @@ -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 { + 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, + ), + ]; + } +} diff --git a/src/pipeline/stellar/rules/events.rule.ts b/src/pipeline/stellar/rules/events.rule.ts new file mode 100644 index 0000000..63897c9 --- /dev/null +++ b/src/pipeline/stellar/rules/events.rule.ts @@ -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 { + 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 }, + ), + ]; + } +} diff --git a/src/pipeline/stellar/rules/optimization.rule.ts b/src/pipeline/stellar/rules/optimization.rule.ts new file mode 100644 index 0000000..96f50a7 --- /dev/null +++ b/src/pipeline/stellar/rules/optimization.rule.ts @@ -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 { + 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 }, + ), + ]; + } +} diff --git a/src/pipeline/stellar/rules/upgradeability.rule.ts b/src/pipeline/stellar/rules/upgradeability.rule.ts new file mode 100644 index 0000000..1192725 --- /dev/null +++ b/src/pipeline/stellar/rules/upgradeability.rule.ts @@ -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 { + 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, + ), + ]; + } +} diff --git a/src/pipeline/stellar/rules/visibility.rule.ts b/src/pipeline/stellar/rules/visibility.rule.ts new file mode 100644 index 0000000..7ceafb9 --- /dev/null +++ b/src/pipeline/stellar/rules/visibility.rule.ts @@ -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 { + 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 }, + ), + ); + } +} diff --git a/src/pipeline/stellar/soroban-pipeline.ts b/src/pipeline/stellar/soroban-pipeline.ts new file mode 100644 index 0000000..758caad --- /dev/null +++ b/src/pipeline/stellar/soroban-pipeline.ts @@ -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, +): Promise { + const executor = createSorobanPipeline(); + + const context: Omit = { + source, + filePath, + config, + }; + + return executor.execute(context); +} diff --git a/src/pipeline/stellar/types.ts b/src/pipeline/stellar/types.ts new file mode 100644 index 0000000..d7be541 --- /dev/null +++ b/src/pipeline/stellar/types.ts @@ -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, +): RuleViolation { + return { + ruleId, + type, + severity, + message, + location: { file: filePath }, + metadata: suggestion ? { ...metadata, suggestion } : metadata, + }; +}