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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ claude.md
mission-control/src/components/datasheets/Generated*.tsx
.playwright-mcp/
solutions/
datasheets/
*.png
**/specs/
.facet/
.DS_Store
14 changes: 13 additions & 1 deletion canary-checker/docs/reference/1-exec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,19 @@ See <CommonLink to="image-variants">Image Variants</CommonLink> for a list of in

## Shell Language

Use a shebang (`#!`) line as the first line of `script` to choose a different shell. The base image includes `python`, `bash`, and `pwsh`.
Use a shebang (`#!`) line as the first line of `script` to choose a different interpreter. The base image includes `bash`, `python`, and `pwsh` (PowerShell). Canary Checker installs additional runtimes, including Bun and UV, when first used.

### Bun

Use `#!/usr/bin/env bun` to run JavaScript or TypeScript with [Bun](https://bun.sh). Canary Checker detects the shebang, installs Bun when necessary, and runs the script with the configured environment and connections.

### Python with UV

Use `#!/usr/bin/env python3` to run Python with [`uv run --quiet`](https://docs.astral.sh/uv/). UV manages the Python runtime, virtual environment, and dependencies declared with [PEP 723](https://peps.python.org/pep-0723/) inline script metadata.

### PowerShell

Use `#! pwsh` or `#!/usr/bin/env pwsh` to run a PowerShell script. Canary Checker installs PowerShell when necessary and runs the script from a `.ps1` file with a non-interactive execution policy.



Expand Down
29 changes: 29 additions & 0 deletions canary-checker/docs/reference/1-folder.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,35 @@ The following example checks a local folder for files matching `pg-backups-.*.zi
```yaml title="postgres-backup-check.yaml" file=<rootDir>/modules/canary-checker/fixtures/datasources/folder_with_filter.yaml
```

## Templated Paths

The `path` field supports Go template expressions, which is particularly useful for S3 and GCS
bucket checks. Instead of scanning an entire bucket prefix on every check run, you can narrow
the listing to a specific date-partitioned or environment-scoped prefix.

```yaml title="templated-s3-path.yaml"
apiVersion: canaries.flanksource.com/v1
kind: Canary
metadata:
name: daily-backup-check
spec:
schedule: "@every 1h"
folder:
- name: check-todays-backups
path: 's3://my-bucket/backups/{{.properties.env}}/{{ now.Format "2006/01/02" }}/'
filter:
maxAge: 24h
minCount: 1
minSize: 10mb
```

:::tip Performance
Templated paths can dramatically improve S3 check performance. A fixed path like `s3://bucket/backups/`
lists **all** objects under that prefix, which can take minutes for buckets with millions of objects.
A templated path like `s3://bucket/backups/{{ now.Format "2006/01/02" }}/` narrows the `ListObjects`
call to a single date partition.
:::

## Result Variables

The following fields are available in `test`, `display`, and `transform` [expressions](../concepts/expressions).
Expand Down
14 changes: 14 additions & 0 deletions canary-checker/docs/reference/1-http.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The HTTP check sends HTTP requests and evaluates the response.
{field: "ntlm", description: "When set to true, authenticates using NTLM v1 protocol", scheme: "bool"},
{field: "ntlmv2", description: "When set to true, authenticates using NTLM v2 protocol", scheme: "bool"},
{field: "crawl", description: "Crawl configuration for following links", scheme: "[Crawl](#crawl-configuration)"},
{field: "dependsOn", description: "Check names that must complete before this check runs. See [Request Chaining](../concepts/request-chaining)", scheme: "[]string"},
{field: "endpoint", description: "Deprecated: Use url instead", scheme: "string", deprecated: true},
]}/>

Expand Down Expand Up @@ -94,6 +95,19 @@ Use `awsSigV4` to sign requests with AWS Signature Version 4.

```

### HAR Capture

HTTP checks can record a [HAR 1.2](http://www.softwareishard.com/blog/har-12-spec/) file containing the requests and responses collected during a Canary run.

Enable HAR capture with the `http.har` Canary property. Set `http.har.location` to choose the output directory.

| Property | Description | Default |
| -------- | ----------- | ------- |
| `http.har` | Enable HAR capture for HTTP traffic in the Canary | `false` |
| `http.har.location` | Directory where the HAR file is written | `.` |

HAR files use the name `<namespace>_<canary>_<timestamp>.har`.

### Result Variables

Result variables can be used in `test`, `display` and `transform` [expressions](../concepts/expressions)
Expand Down
22 changes: 1 addition & 21 deletions common/src/components/AuditDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,7 @@ import {
import { HiUserGroup, HiShieldCheck, HiKey } from 'react-icons/hi2';
import { FaHistory } from 'react-icons/fa';
import BoxNode from './diagrams/BoxNode';

const COLORS = {
primary: '#2d7de4',
background: '#f7fbfe',
accent: '#1069dc',
muted: '#62758a',
};

const primaryArrowProps = {
color: COLORS.primary,
strokeWidth: 3,
headSize: 4,
dashness: { strokeLen: 10, nonStrokeLen: 5, animation: 1 },
} as const;

const secondaryArrowProps = {
color: COLORS.muted,
strokeWidth: 2,
headSize: 3,
dashness: { strokeLen: 6, nonStrokeLen: 4, animation: 1 },
} as const;
import { COLORS, primaryArrowProps, secondaryArrowProps } from './diagrams/diagramUtils';

interface IconGridProps {
items: Array<{ Icon: React.ComponentType<{ className?: string }>; label?: string }>;
Expand Down
211 changes: 211 additions & 0 deletions common/src/components/CustomScraperDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React, { useId } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';
import Xarrow from 'react-xarrows';
import BoxNode from './diagrams/BoxNode';
import {
ConfigDbWhite,
MissionControlWhite,
Http,
Postgres,
SqlServer,
Clickhouse,
Aws,
Azure,
GoogleCloud,
AzureAd,
Github,
} from '@flanksource/icons/mi';
import { HiUserGroup, HiShieldCheck, HiKey } from 'react-icons/hi2';
import { FaHistory, FaLightbulb, FaDatabase, FaFileCode, FaTerminal } from 'react-icons/fa';
import { COLORS, primaryArrowProps } from './diagrams/diagramUtils';

function ProtocolBox({ id, title, icons }: { id: string; title: string; icons: Array<{ Icon: React.ComponentType<{ className?: string }>; label: string }> }) {
return (
<div id={id}>
<BoxNode
title={title}
headerColor={COLORS.primary}
bodyColor={COLORS.background}
borderColor={COLORS.primary}
border="solid"
compact
>
<div className="grid gap-2" style={{ gridTemplateColumns: `repeat(3, minmax(0, 1fr))` }}>
{icons.map(({ Icon, label }) => (
<div key={label} className="flex flex-col items-center">
<Icon className="w-6 h-6" />
<span className="text-[9px] mt-0.5" style={{ color: COLORS.muted }}>{label}</span>
</div>
))}
</div>
</BoxNode>
</div>
);
}

function DataSourcesColumn({ idFile, idHttp, idSql }: { idFile: string; idHttp: string; idSql: string }) {
return (
<div className="flex flex-col items-center gap-4">
<ProtocolBox id={idFile} title="File / Exec" icons={[
{ Icon: FaFileCode, label: 'File' },
{ Icon: FaTerminal, label: 'Exec' },
{ Icon: Github, label: 'Git' },
]} />
<ProtocolBox id={idHttp} title="HTTP" icons={[
{ Icon: Http, label: 'REST' },
{ Icon: AzureAd, label: 'Entra ID' },
{ Icon: Aws, label: 'AWS' },
]} />
<ProtocolBox id={idSql} title="SQL" icons={[
{ Icon: Postgres, label: 'Postgres' },
{ Icon: SqlServer, label: 'SQL Server' },
{ Icon: Clickhouse, label: 'Clickhouse' },
]} />
</div>
);
}

function ScraperBox({ id }: { id: string }) {
return (
<div
id={id}
className="flex items-center gap-2 rounded-lg px-5 py-3 shadow-lg border-2"
style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)',
borderColor: COLORS.primary,
boxShadow: '0 4px 14px rgba(59, 130, 246, 0.4)',
}}
>
<ConfigDbWhite className="w-7 h-7 text-white" />
<div className="flex flex-col">
<span className="text-white font-bold text-sm">ScrapeConfig</span>
<span className="text-blue-200 text-[10px]">full: true</span>
</div>
</div>
);
}

function JsonOutputBox({ id }: { id: string }) {
const lines = [
{ text: '{', indent: 0 },
{ text: '"id": "db-prod-001",', indent: 1 },
{ text: '"type": "Database",', indent: 1 },
{ text: '"config": { ... },', indent: 1, highlight: true },
{ text: '"changes": [ ... ],', indent: 1, highlight: true },
{ text: '"access": [ ... ],', indent: 1, highlight: true },
{ text: '"logs": [ ... ],', indent: 1, highlight: true },
{ text: '"analysis": [ ... ],', indent: 1, highlight: true },
{ text: '"users": [ ... ]', indent: 1, highlight: true },
{ text: '}', indent: 0 },
];

return (
<div id={id}>
<BoxNode
title="Scraper Output"
headerColor={COLORS.muted}
bodyColor="#1e293b"
borderColor={COLORS.muted}
border="solid"
minWidth="180px"
>
<pre className="m-0 p-0 text-[10px] leading-[1.6] font-mono" style={{ background: 'transparent' }}>
{lines.map(({ text, indent, highlight }, i) => (
<div key={i} style={{ paddingLeft: `${indent * 12}px`, color: highlight ? '#93c5fd' : '#94a3b8' }}>
{text}
</div>
))}
</pre>
</BoxNode>
</div>
);
}

function MissionControlBox({ id }: { id: string }) {
return (
<div
id={id}
className="rounded-2xl overflow-hidden border-2 shadow-2xl"
style={{ borderColor: COLORS.primary, backgroundColor: COLORS.background }}
>
<div className="px-5 py-2.5 text-center" style={{ backgroundColor: COLORS.primary }}>
<div className="flex items-center justify-center gap-2">
<MissionControlWhite className="w-5 h-5 text-white" />
<span className="text-white text-sm font-bold tracking-wide">Mission Control</span>
</div>
</div>
<div className="p-3 flex flex-col gap-1.5">
{[
{ Icon: FaDatabase, label: 'Config Item' },
{ Icon: FaHistory, label: 'Changes' },
{ Icon: HiKey, label: 'Access Records' },
{ Icon: HiUserGroup, label: 'Access Logs' },
{ Icon: FaLightbulb, label: 'Insights' },
{ Icon: HiShieldCheck, label: 'Users & Roles' },
].map(({ Icon, label }) => (
<div key={label} className="flex items-center gap-2 rounded-lg px-3 py-1.5 border"
style={{ backgroundColor: '#ffffff', borderColor: COLORS.primary }}>
<Icon className="w-3.5 h-3.5" style={{ color: COLORS.accent }} />
<span className="text-[10px] font-medium" style={{ color: COLORS.muted }}>{label}</span>
</div>
))}
</div>
</div>
);
}

function TransformPipeline({ id }: { id: string }) {
const steps = ['Exclude', 'Mask', 'Relationships', 'Changes'];
return (
<div id={id} className="flex items-center gap-1">
{steps.map((step, i) => (
<React.Fragment key={step}>
<div
className="rounded px-2 py-1 text-[9px] font-medium border"
style={{ borderColor: '#a78bfa', backgroundColor: '#f5f3ff', color: '#6d28d9' }}
>
{step}
</div>
{i < steps.length - 1 && <span className="text-[10px]" style={{ color: '#a78bfa' }}>→</span>}
</React.Fragment>
))}
</div>
);
}

interface CustomScraperDiagramProps {
className?: string;
}

function CustomScraperDiagramInner({ className }: CustomScraperDiagramProps) {
const prefix = useId();
const id = (name: string) => `${prefix}-${name}`;

return (
<div className={`${className || ''} relative flex items-center justify-center gap-10 py-6`}>
<DataSourcesColumn idFile={id('file')} idHttp={id('http')} idSql={id('sql')} />

<div className="flex flex-col items-center gap-3">
<ScraperBox id={id('scraper')} />
<TransformPipeline id={id('transform')} />
</div>

<JsonOutputBox id={id('json')} />
<MissionControlBox id={id('mc')} />

<Xarrow start={id('file')} end={id('scraper')} {...primaryArrowProps} path="smooth" startAnchor="right" endAnchor={{ position: 'left', offset: { y: -5 } }} />
<Xarrow start={id('http')} end={id('scraper')} {...primaryArrowProps} path="straight" startAnchor="right" endAnchor="left" />
<Xarrow start={id('sql')} end={id('scraper')} {...primaryArrowProps} path="smooth" startAnchor="right" endAnchor={{ position: 'left', offset: { y: 5 } }} />
<Xarrow start={id('scraper')} end={id('json')} {...primaryArrowProps} path="straight" startAnchor="right" endAnchor="left" />
<Xarrow start={id('json')} end={id('mc')} {...primaryArrowProps} path="straight" startAnchor="right" endAnchor="left" />
</div>
);
}

export default function CustomScraperDiagram(props: CustomScraperDiagramProps) {
return (
<BrowserOnly fallback={<div className="w-full" />}>
{() => <CustomScraperDiagramInner {...props} />}
</BrowserOnly>
);
}
37 changes: 1 addition & 36 deletions common/src/components/EntraDataFlowDiagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,7 @@ import {
MissionControlWhite,
} from '@flanksource/icons/mi';
import BoxNode from './diagrams/BoxNode';

const COLORS = {
primary: '#2d7de4',
background: '#f7fbfe',
accent: '#1069dc',
muted: '#62758a',
outputBorder: '#10b981',
};

const pillStyle: React.CSSProperties = {
color: COLORS.muted,
backgroundColor: COLORS.background,
border: `1px solid ${COLORS.primary}`,
};

const primaryArrowProps = {
color: COLORS.primary,
strokeWidth: 3,
headSize: 4,
dashness: { strokeLen: 10, nonStrokeLen: 5, animation: 1 },
} as const;

const secondaryArrowProps = {
color: COLORS.muted,
strokeWidth: 2,
headSize: 3,
dashness: { strokeLen: 6, nonStrokeLen: 4, animation: 1 },
} as const;

function NodePill({ children }: { children: React.ReactNode }) {
return (
<div className="text-[10px] rounded px-2 py-1 text-center" style={pillStyle}>
{children}
</div>
);
}
import { COLORS, primaryArrowProps, secondaryArrowProps, NodePill } from './diagrams/diagramUtils';

function NodeSection({ title, items, id }: { title: string; items: string[]; id?: string }) {
return (
Expand Down
Loading
Loading