diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..08b0ad0 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,20 @@ +--- +name: Deploy website +on: + push: + branches: [main] + paths: [website/**] + workflow_dispatch: + +permissions: + contents: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: hahwul/hwaro@main + with: + build_dir: website + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..364fdec --- /dev/null +++ b/website/.gitignore @@ -0,0 +1 @@ +public/ diff --git a/website/AGENTS.md b/website/AGENTS.md new file mode 100644 index 0000000..ce0e41a --- /dev/null +++ b/website/AGENTS.md @@ -0,0 +1,76 @@ +# AGENTS.md - AI Agent Instructions for Hwaro Site + +This document provides instructions for AI agents working on this Hwaro-generated website. + +## Project Overview + +This is a static website built with [Hwaro](https://github.com/hahwul/hwaro), a fast and lightweight static site generator written in Crystal. + +## Essential Commands + +| Command | Description | +|---------|-------------| +| `hwaro build` | Build the site to `public/` directory | +| `hwaro serve` | Start development server with live reload | +| `hwaro new ` | Create new content from archetype | +| `hwaro deploy` | Deploy the site (requires configuration) | +| `hwaro build --drafts` | Include draft content | +| `hwaro serve -p 8080` | Serve on custom port (default: 3000) | +| `hwaro build --base-url "https://example.com"` | Set base URL for production | + +## Directory Structure + +``` +. +├── config.toml # Site configuration +├── content/ # Markdown content files +│ ├── index.md # Homepage (single file, no underscore) +│ ├── about.md # Standalone page +│ └──
/ # Section directory (posts/, guide/, chapter-1/, …) +│ ├── _index.md # Section landing page (underscore-prefixed) +│ └── *.md # Pages within the section +├── templates/ # Jinja2 templates (Crinja) +│ ├── header.html # Shared + open +│ ├── footer.html # Shared / close +│ ├── page.html # Page template +│ ├── section.html # Section listing template +│ ├── 404.html # Not-found page +│ ├── partials/ # Reusable fragments (nav, search, sidebar) +│ └── shortcodes/ # Shortcode templates +├── static/ # Static assets (copied as-is) +└── archetypes/ # Content templates for `hwaro new` +``` + +## Notes for AI Agents + +1. **Front matter** can be TOML (`+++`), YAML (`---`), or JSON (`{...}` at file start). Pick one per file and keep delimiters matched. +2. **Rendered content** is `{{ content }}` in templates (already-safe HTML — no extra `| safe` needed). +3. **Custom metadata** is `page.extra.field`, not `page.params.field`. +4. **Always preview** with `hwaro serve` before committing. +5. **Validate front matter syntax** (TOML, YAML, or JSON) and `config.toml` after edits. +6. **Use `{{ base_url }}` prefix** for URLs in templates. +7. **Escape user content** with `{{ value | e }}` (or `| escape`) in templates. + +## Full Reference + +For detailed documentation on content, templates, configuration, and more: + +- [Hwaro Documentation](https://hwaro.hahwul.com) +- [Configuration Guide](https://hwaro.hahwul.com/start/config/) +- [Full LLM Reference](https://hwaro.hahwul.com/llms-full.txt) — comprehensive reference optimized for AI agents + +To generate the full embedded AGENTS.md locally, run: +``` +hwaro tool agents-md --local --write +``` + +## Site-Specific Instructions + +This site is a trilingual (en at root, /ko/, /ja/) comic field guide to DevSecOps, deployed to https://devsecops.hahwul.com (CNAME lives in `static/CNAME`). + +- Every content page exists three times: `foo.md`, `foo.ko.md`, `foo.ja.md`. Keep the shortcode structure of the three files identical; only translate prose, dialogue, and front matter strings. UI strings live in `i18n/{en,ko,ja}.toml`. +- The comic system is shortcode-driven: `strip` (panel column/row), `panel` (args: tint="sec"|"warm", halftone, center), `bubble` (args: who, dir, kind, name), `scene` (args: name, pose, alt, size, bg="board"|"window"|"gears"|"radar" for a muted backdrop), `caps` and `resources` (wrap markdown lists), `team`/`member`, `loop_diagram`, `episode_rail`, `finale`. +- Characters are inline SVG partials in `templates/partials/svg/` (char-dev, char-sec, char-ops, char-bug + trio/loop/icon). Rules: strokes `var(--ink)`, fills only from `--c-*`/`--paper` tokens plus the shading tokens (`--c-*-sh`, `--c-*-hi`, `--shade`, `--ground`), no `` elements ever (all dialogue must stay translatable HTML), poses are Jinja branches. Every character gets a ground shadow, one `-sh` shading pass, and one `-hi` sheen so they never read as flat pasted shapes. +- Chapter pages use `template = "chapter"` with `[extra] phase/episode/hook/has_tools`. The loop order lives in `data/chapters.yml`; the tool list in `data/tools.yml` (with `description_ko`/`description_ja`). +- Design constraints: no em-dashes anywhere in visible copy (any language), one accent (mint teal) + one support hue (apricot), light/dark via `light-dark()` tokens in `static/css/site.css`, motion gated behind `html.js` + `prefers-reduced-motion: no-preference`, no `window.addEventListener("scroll")`. +- Tools content mirrors `../tools/README.md`; when tools are added to the repo table, add them to `data/tools.yml` too. \ No newline at end of file diff --git a/website/archetypes/default.md b/website/archetypes/default.md new file mode 100644 index 0000000..650fb40 --- /dev/null +++ b/website/archetypes/default.md @@ -0,0 +1,7 @@ ++++ +title = "{{ title }}" +date = "{{ date }}" +draft = {{ draft }} +description = "{{ description }}" +tags = {{ tags }} ++++ diff --git a/website/config.toml b/website/config.toml new file mode 100644 index 0000000..a8ba997 --- /dev/null +++ b/website/config.toml @@ -0,0 +1,53 @@ +title = "DevSecOps" +description = "A comic field guide to DevSecOps. Learn the six phases of the loop in illustrated episodes, then pick your tools." +base_url = "https://devsecops.hahwul.com" +default_language = "en" + +[languages.en] +language_name = "English" +weight = 1 + +[languages.ko] +language_name = "한국어" +weight = 2 +generate_feed = false + +[languages.ja] +language_name = "日本語" +weight = 3 +generate_feed = false + +[plugins] +processors = ["markdown"] + +[content.files] +allow_extensions = ["jpg", "jpeg", "png", "gif", "svg", "webp"] + +[highlight] +enabled = true +mode = "server" +theme = "github" +use_cdn = true +copy = true + +[sitemap] +enabled = true + +[feeds] +enabled = false + +[search] +enabled = false + +[og] +type = "website" +twitter_card = "summary_large_image" +default_image = "/images/og.png" + +[markdown] +emoji = false +task_lists = false +definition_lists = false +footnotes = false +mermaid = false +math = false diff --git a/website/content/about.ja.md b/website/content/about.ja.md new file mode 100644 index 0000000..bd8160a --- /dev/null +++ b/website/content/about.ja.md @@ -0,0 +1,22 @@ ++++ +title = "概要" +description = "このマンガフィールドガイドとは何か、どこから来たのか、どう貢献できるのか。" ++++ + +{% panel(halftone=true, center=true) %} +{{ scene(name="trio", alt="並んで立つDev、Sec、Ops") }} +{% endpanel %} + +このサイトは**DevSecOps**のためのマンガフィールドガイドです。DevSecOpsは、セキュリティを最後に付け足すのではなく、ソフトウェア開発ライフサイクルのすべてのフェーズに組み込む文化であり実践です。6つのイラストエピソードが設計から運用までループを一緒に歩き、それぞれに概念、ハンズオンの例、厳選した読み物、そして合わせて使えるツールを詰め込みました。 + +## どこから来たのか + +ここにあるすべては、2020年に始まったオープンなロードマップ兼ツールコレクション[hahwul/DevSecOps](https://github.com/hahwul/DevSecOps)から育ちました。ロードマップもリソースリストも30個のツールアーセナルもコミュニティがリポジトリで管理しており、このサイトはそれらを物語として語り直しています。 + +## コントリビューション + +壊れたリンク、抜けているツール、もっと良い説明を見つけたら、[IssueかPull Requestをどうぞ](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md)。ツールの追加はまずリポジトリに入り、そこから[ツール](/ja/tools/)ページへ流れてきます。 + +## コロフォン + +Crystal製の静的サイトジェネレータ[Hwaro](https://github.com/hahwul/hwaro)で作りました。キャラクターは手描きのSVG、フォントはあなたのシステムのもの、そしてサイト全体がライトモードとダークモードの両方で動きます。元のリポジトリと同じくMITライセンスです。 diff --git a/website/content/about.ko.md b/website/content/about.ko.md new file mode 100644 index 0000000..d2fa058 --- /dev/null +++ b/website/content/about.ko.md @@ -0,0 +1,22 @@ ++++ +title = "소개" +description = "이 만화 필드 가이드가 무엇인지, 어디에서 왔는지, 어떻게 기여할 수 있는지." ++++ + +{% panel(halftone=true, center=true) %} +{{ scene(name="trio", alt="나란히 서 있는 Dev, Sec, Ops") }} +{% endpanel %} + +이 사이트는 **DevSecOps**를 위한 만화 필드 가이드예요. DevSecOps는 보안을 마지막에 덧붙이는 대신 소프트웨어 개발 라이프사이클의 모든 단계에 심는 문화이자 실천입니다. 여섯 개의 일러스트 에피소드가 설계부터 운영까지 루프를 함께 걸으며, 각 에피소드마다 개념, 실습 예제, 엄선된 읽을거리, 어울리는 도구를 담았어요. + +## 어디에서 왔나요 + +이곳의 모든 내용은 2020년에 시작된 오픈 로드맵이자 도구 모음인 [hahwul/DevSecOps](https://github.com/hahwul/DevSecOps)에서 자랐어요. 로드맵과 리소스 목록, 30개 도구 아스널은 커뮤니티가 저장소에서 관리하고, 이 사이트는 그것을 이야기로 다시 들려줍니다. + +## 기여하기 + +깨진 링크, 빠진 도구, 더 나은 설명을 찾으셨나요? [이슈나 풀 리퀘스트를 열어 주세요](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md). 도구 추가는 저장소에 먼저 반영되고, 그다음 [도구](/ko/tools/) 페이지로 흘러옵니다. + +## 콜로폰 + +Crystal로 작성된 정적 사이트 생성기 [Hwaro](https://github.com/hahwul/hwaro)로 만들었어요. 캐릭터는 손으로 그린 SVG이고, 글꼴은 여러분 시스템의 것이며, 사이트 전체가 라이트와 다크 모드에서 동작합니다. 원본 저장소처럼 MIT 라이선스예요. diff --git a/website/content/about.md b/website/content/about.md new file mode 100644 index 0000000..4717856 --- /dev/null +++ b/website/content/about.md @@ -0,0 +1,22 @@ ++++ +title = "About" +description = "What this comic field guide is, where it comes from, and how to contribute." ++++ + +{% panel(halftone=true, center=true) %} +{{ scene(name="trio", alt="Dev, Sec, and Ops standing together") }} +{% endpanel %} + +This site is a comic field guide to **DevSecOps**: the practice of building security into every phase of the software development lifecycle instead of bolting it on at the end. Six illustrated episodes walk the loop from Design to Operate, each with the concepts, one hands-on example, curated reading, and the tools to match. + +## Where it comes from + +Everything here grows out of [hahwul/DevSecOps](https://github.com/hahwul/DevSecOps), an open roadmap and tool collection started in 2020. The roadmap, the resource lists, and the 30-tool arsenal are maintained there by the community; this site retells them as a story. + +## Contributing + +Found a broken link, a missing tool, or a better explanation? [Open an issue or a pull request](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md). Tool additions land in the repository first and flow to the [Tools](/tools/) page from there. + +## Colophon + +Built with [Hwaro](https://github.com/hahwul/hwaro), a static site generator written in Crystal. The characters are hand-drawn SVG, the fonts are your system's own, and the whole site works in light and dark. MIT licensed, like the repository it comes from. diff --git a/website/content/build/_index.ja.md b/website/content/build/_index.ja.md new file mode 100644 index 0000000..7ddbf8e --- /dev/null +++ b/website/content/build/_index.ja.md @@ -0,0 +1,87 @@ ++++ +title = "ビルド" +description = "プッシュのたびにSAST、SCA、シークレットスキャン。小言はパイプラインに任せよう。" +template = "chapter" +weight = 3 + +[extra] +phase = "build" +episode = 3 +hook = "小言はパイプラインに任せよう。" +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="パイプラインを調整するOps") }} + +{% bubble(who="ops", name="Ops") %}プッシュして。2分後にパイプラインが真実を教えてくれる。{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="シークレットスキャナーに捕まったバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}シークレットスキャナー?!僕のAPIキーコレクションのこと、誰が教えたの?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +すべてのプッシュは、バグを自動で捕まえるチャンスです。**SAST**はソースから脆弱なパターンを読み取り、**SCA**は依存関係を既知のCVEと突き合わせ、**シークレットスキャン**はコミットしてはいけなかったトークンを狩ります。テスト実行に**IAST**エージェントを載せて、内側からの視界を得るチームもあります。 + +ビルドシステム自体も攻撃対象面です。CIジョブが乗っ取られれば、あなたの代わりに署名も公開もデプロイもできてしまう。パイプラインは実質的に本番環境。本番と同じように固めましょう。 + +{% caps() %} +- SAST +- SCA +- シークレット管理 +- IAST +{% endcaps %} + +## 実践では + +すべてのプッシュとプルリクエストで走る、最小限のセキュリティジョブです。 + +{% raw %} +```yaml +name: security +on: [push, pull_request] + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Scan for leaked secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Static analysis + run: | + pip install semgrep + semgrep ci --config auto +``` +{% endraw %} + +まずはデフォルトで始めて、ルールをコードベースに合わせて調整していきましょう。うるさいスキャナーは無視され、静かで正確なスキャナーはチームの一員になります。 + +{% alert(type="warning", title="ビルドを落とそう") %}警告を出すだけの検出結果は、みんながスクロールで読み飛ばす検出結果です。クリティカルはビルドを落とすようにしましょう。{% endalert %} + +## さらに読む + +{% resources() %} +- [SonarQubeでソースコードをSASTスキャン](https://medium.com/nycdev/scan-your-source-code-for-vulnerabilities-using-static-application-security-testing-sast-with-5f8ee1fdf9aa) +- [GitHubのサードパーティコードスキャンツール](https://github.blog/2020-10-05-announcing-third-party-code-scanning-tools-static-analysis-and-developer-security-training/) +- [OWASP DSOVSが定義するSASTレベル](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/CODE-004-Static-Application-Security-Testing-SAST.md) +- [GitHub Actionsのセキュリティ強化ガイド](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [GitHub Actionsセキュリティベストプラクティス (Salesforce)](https://engineering.salesforce.com/github-actions-security-best-practices-b8f9df5c75f5) +- [GitHub Actionsセキュリティチートシート (GitGuardian)](https://blog.gitguardian.com/github-actions-security-cheat-sheet/) +- [Securing Jenkins](https://www.jenkins.io/doc/book/security/) +- [SANSによるJenkins CIシステムのセキュリティ](https://www.sans.org/white-papers/36872/) +{% endresources %} diff --git a/website/content/build/_index.ko.md b/website/content/build/_index.ko.md new file mode 100644 index 0000000..280cabf --- /dev/null +++ b/website/content/build/_index.ko.md @@ -0,0 +1,87 @@ ++++ +title = "빌드" +description = "푸시할 때마다 SAST, SCA, 시크릿 스캔. 잔소리는 파이프라인에게 맡기세요." +template = "chapter" +weight = 3 + +[extra] +phase = "build" +episode = 3 +hook = "잔소리는 파이프라인에게 맡기세요." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="파이프라인을 조율하는 Ops") }} + +{% bubble(who="ops", name="Ops") %}푸시하세요. 2분 뒤에 파이프라인이 진실을 말해 줄 거예요.{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="시크릿 스캐너에 걸린 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}시크릿 스캐너라니?! 내 API 키 컬렉션은 누가 알려 줬어요?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +모든 푸시는 버그를 자동으로 잡을 기회예요. **SAST**는 소스에서 취약한 패턴을 읽어 내고, **SCA**는 의존성을 알려진 CVE와 대조하고, **시크릿 스캔**은 커밋되면 안 됐을 토큰을 사냥합니다. 테스트 실행에 **IAST** 에이전트를 붙여 내부 시야를 확보하는 팀도 있어요. + +빌드 시스템 자체도 공격 표면이에요. CI 잡이 장악당하면 여러분 대신 서명하고, 게시하고, 배포할 수 있죠. 파이프라인은 사실상 운영 환경이니, 운영 환경처럼 단단하게 지키세요. + +{% caps() %} +- SAST +- SCA +- 시크릿 관리 +- IAST +{% endcaps %} + +## 실전에서는 + +모든 푸시와 풀 리퀘스트에서 도는 최소한의 보안 잡이에요. + +{% raw %} +```yaml +name: security +on: [push, pull_request] + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Scan for leaked secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Static analysis + run: | + pip install semgrep + semgrep ci --config auto +``` +{% endraw %} + +기본 설정으로 시작하고, 규칙은 코드베이스에 맞게 다듬어 가세요. 시끄러운 스캐너는 무시당하지만, 조용하고 정확한 스캐너는 팀의 일원이 됩니다. + +{% alert(type="warning", title="빌드를 깨뜨리세요") %}경고만 찍고 지나가는 발견은 모두가 스크롤해서 지나치는 발견이에요. 크리티컬은 빌드를 깨뜨리게 하세요.{% endalert %} + +## 더 읽어보기 + +{% resources() %} +- [SonarQube로 소스 코드 SAST 스캔하기](https://medium.com/nycdev/scan-your-source-code-for-vulnerabilities-using-static-application-security-testing-sast-with-5f8ee1fdf9aa) +- [GitHub의 서드파티 코드 스캐닝 도구](https://github.blog/2020-10-05-announcing-third-party-code-scanning-tools-static-analysis-and-developer-security-training/) +- [OWASP DSOVS가 정의한 SAST 레벨](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/CODE-004-Static-Application-Security-Testing-SAST.md) +- [GitHub Actions 보안 강화 가이드](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [GitHub Actions 보안 모범 사례 (Salesforce)](https://engineering.salesforce.com/github-actions-security-best-practices-b8f9df5c75f5) +- [GitHub Actions 보안 치트 시트 (GitGuardian)](https://blog.gitguardian.com/github-actions-security-cheat-sheet/) +- [Securing Jenkins](https://www.jenkins.io/doc/book/security/) +- [SANS의 Jenkins CI 시스템 보안](https://www.sans.org/white-papers/36872/) +{% endresources %} diff --git a/website/content/build/_index.md b/website/content/build/_index.md new file mode 100644 index 0000000..7b44fcb --- /dev/null +++ b/website/content/build/_index.md @@ -0,0 +1,87 @@ ++++ +title = "Build" +description = "SAST, SCA, and secret scanning on every push: let the pipeline do the nagging." +template = "chapter" +weight = 3 + +[extra] +phase = "build" +episode = 3 +hook = "Let the pipeline do the nagging." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="Ops tunes the pipeline") }} + +{% bubble(who="ops", name="Ops") %}Push it. The pipeline will tell us the truth in two minutes.{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="The Bug is caught by the secret scanner") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}A secret scanner?! Who told them about my API key collection?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +Every push is a chance to catch the Bug automatically. **SAST** reads your source for vulnerable patterns, **SCA** checks your dependencies against known CVEs, and **secret scanning** hunts for tokens that were never meant to be committed. Some teams add **IAST** agents to their test runs for coverage from the inside. + +The build system itself is part of the attack surface: a compromised CI job can sign, publish, and deploy on your behalf. Harden the pipeline like production, because it effectively is. + +{% caps() %} +- SAST +- SCA +- Secret Management +- IAST +{% endcaps %} + +## In practice + +A minimal security job that runs on every push and pull request: + +{% raw %} +```yaml +name: security +on: [push, pull_request] + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Scan for leaked secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Static analysis + run: | + pip install semgrep + semgrep ci --config auto +``` +{% endraw %} + +Start with the defaults, then tune the rules to your codebase. A noisy scanner gets ignored; a quiet, accurate one becomes part of the team. + +{% alert(type="warning", title="Fail the build") %}A finding that only prints a warning is a finding everyone scrolls past. Make criticals break the build.{% endalert %} + +## Keep reading + +{% resources() %} +- [Scan Source Code with SAST and SonarQube](https://medium.com/nycdev/scan-your-source-code-for-vulnerabilities-using-static-application-security-testing-sast-with-5f8ee1fdf9aa) +- [Third-party Code Scanning Tools on GitHub](https://github.blog/2020-10-05-announcing-third-party-code-scanning-tools-static-analysis-and-developer-security-training/) +- [SAST Levels Defined by OWASP DSOVS](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/CODE-004-Static-Application-Security-Testing-SAST.md) +- [Security Hardening for GitHub Actions](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [GitHub Actions Security Best Practices (Salesforce)](https://engineering.salesforce.com/github-actions-security-best-practices-b8f9df5c75f5) +- [GitHub Actions Security Cheat Sheet (GitGuardian)](https://blog.gitguardian.com/github-actions-security-cheat-sheet/) +- [Securing Jenkins](https://www.jenkins.io/doc/book/security/) +- [Securing Jenkins CI Systems (SANS)](https://www.sans.org/white-papers/36872/) +{% endresources %} diff --git a/website/content/deploy/_index.ja.md b/website/content/deploy/_index.ja.md new file mode 100644 index 0000000..2041e5a --- /dev/null +++ b/website/content/deploy/_index.ja.md @@ -0,0 +1,61 @@ ++++ +title = "デプロイ" +description = "デフォルトからハードニングされたホストと安全な設定。" +template = "chapter" +weight = 5 + +[extra] +phase = "deploy" +episode = 5 +hook = "扉を開ける前に、デフォルトから安全に。" +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="ハードニングチェックリストを適用するOps") }} + +{% bubble(who="ops", name="Ops") %}CISプロファイル適用済み。デバッグポートは閉鎖。デフォルトから安全です。{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="どのドアも施錠されていて戸惑うバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}施錠。施錠。これも施錠。誰がこんなふうにリリースするの?!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +完璧に書かれたアプリでも、設定を誤った箱の上では崩れます。**ハードニング**とは、ホストもクラスタもランタイムも、勘ではなくベンチマークに従わせること。「ロックダウンした」の基準で揉めないために**CISベンチマーク**があります。 + +**設定**が残りの物語です。安全なデフォルト、本番にデバッグエンドポイントを残さない、シークレットはイメージに焼き込まず実行時に注入、そしてそのすべての自動化。手作業で適用したものは、必ずズレていくからです。 + +{% caps() %} +- ハードニング +- 設定 +{% endcaps %} + +## 実践では + +イメージの中に実際に何が入っているかでリリースを止め、プラットフォームはベンチマークで採点しましょう。 + +```bash +# 既知のクリティカル脆弱性を含むイメージはリリース拒否 +trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/shop:1.4.2 + +# CIS Kubernetesベンチマークでクラスタを採点 +kube-bench run --benchmark cis-1.8 +``` + +どちらのコマンドもデプロイジョブに収まります。それがポイント。安全な道と速い道は、同じパイプラインであるべきです。 + +## さらに読む + +{% resources() %} +- [CISベンチマーク](https://www.cisecurity.org/cis-benchmarks/) +- [KubernetesでのDevSecOps (Microsoft)](https://cloudblogs.microsoft.com/opensource/2019/07/22/devsecops-in-kubernetes/) +- [Docker Scoutによるイメージスキャン](https://docs.docker.com/scout/) +{% endresources %} diff --git a/website/content/deploy/_index.ko.md b/website/content/deploy/_index.ko.md new file mode 100644 index 0000000..fca4e8a --- /dev/null +++ b/website/content/deploy/_index.ko.md @@ -0,0 +1,61 @@ ++++ +title = "배포" +description = "기본값부터 하드닝된 호스트와 안전한 설정." +template = "chapter" +weight = 5 + +[extra] +phase = "deploy" +episode = 5 +hook = "문을 열기 전에, 기본값부터 안전하게." +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="하드닝 체크리스트를 적용하는 Ops") }} + +{% bubble(who="ops", name="Ops") %}CIS 프로파일 적용 완료. 디버그 포트 차단. 기본값이 안전해요.{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="모든 문이 잠겨 있어 당황한 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}잠김. 잠김. 이것도 잠김. 대체 누가 이렇게 배포해요?!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +완벽하게 짠 앱도 잘못 설정된 서버 위에서는 무너져요. **하드닝**은 호스트, 클러스터, 런타임이 감이 아니라 벤치마크를 따르게 하는 것. "잠갔다"의 기준을 두고 다투지 않도록 **CIS 벤치마크**가 존재합니다. + +**설정**이 나머지 이야기예요. 안전한 기본값, 운영 환경에 디버그 엔드포인트 금지, 이미지에 굽지 않고 런타임에 주입하는 시크릿, 그리고 이 모든 것의 자동화. 손으로 적용한 것은 반드시 어긋나게 되어 있으니까요. + +{% caps() %} +- 하드닝 +- 설정 +{% endcaps %} + +## 실전에서는 + +이미지 안에 실제로 뭐가 들었는지로 릴리스를 막고, 플랫폼은 벤치마크로 점검하세요. + +```bash +# 알려진 크리티컬 취약점이 있는 이미지는 배포 거부 +trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/shop:1.4.2 + +# CIS 쿠버네티스 벤치마크로 클러스터 채점 +kube-bench run --benchmark cis-1.8 +``` + +두 명령 모두 배포 잡 안에 들어가요. 그게 핵심입니다. 안전한 길과 빠른 길이 같은 파이프라인이어야 하니까요. + +## 더 읽어보기 + +{% resources() %} +- [CIS 벤치마크](https://www.cisecurity.org/cis-benchmarks/) +- [쿠버네티스에서의 DevSecOps (Microsoft)](https://cloudblogs.microsoft.com/opensource/2019/07/22/devsecops-in-kubernetes/) +- [Docker Scout로 하는 이미지 스캔](https://docs.docker.com/scout/) +{% endresources %} diff --git a/website/content/deploy/_index.md b/website/content/deploy/_index.md new file mode 100644 index 0000000..66f1905 --- /dev/null +++ b/website/content/deploy/_index.md @@ -0,0 +1,61 @@ ++++ +title = "Deploy" +description = "Hardened hosts and safe configuration by default, before the doors open." +template = "chapter" +weight = 5 + +[extra] +phase = "deploy" +episode = 5 +hook = "Safe by default, before the doors open." +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-ops", pose="gear", bg="gears", alt="Ops applies the hardening checklist") }} + +{% bubble(who="ops", name="Ops") %}CIS profile applied. Debug ports closed. Defaults are safe.{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="caught", alt="The Bug finds every door locked") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}Locked. Locked. Also locked. Who ships like this?!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +A perfectly written app can still ship on a badly configured box. **Hardening** means the host, cluster, and runtime follow a benchmark instead of gut feeling; the **CIS Benchmarks** exist so nobody has to argue about what "locked down" means. + +**Configuration** is the rest of the story: secure defaults, no debug endpoints in production, secrets injected at runtime instead of baked into images, and automation for all of it, because anything applied by hand will drift. + +{% caps() %} +- Hardening +- Configuration +{% endcaps %} + +## In practice + +Gate the release on what is actually inside the image, then check the platform against a benchmark: + +```bash +# Refuse to ship images with known critical vulnerabilities +trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/shop:1.4.2 + +# Score the cluster against the CIS Kubernetes benchmark +kube-bench run --benchmark cis-1.8 +``` + +Both commands fit in a deploy job, which is the point: the safe path and the fast path should be the same pipeline. + +## Keep reading + +{% resources() %} +- [CIS Benchmarks](https://www.cisecurity.org/cis-benchmarks/) +- [DevSecOps in Kubernetes (Microsoft)](https://cloudblogs.microsoft.com/opensource/2019/07/22/devsecops-in-kubernetes/) +- [Image Scanning with Docker Scout](https://docs.docker.com/scout/) +{% endresources %} diff --git a/website/content/design/_index.ja.md b/website/content/design/_index.ja.md new file mode 100644 index 0000000..6121f49 --- /dev/null +++ b/website/content/design/_index.ja.md @@ -0,0 +1,75 @@ ++++ +title = "設計" +description = "脅威モデリングとセキュアSDLC。セキュリティは最初のコミットの前に始まります。" +template = "chapter" +weight = 1 + +[extra] +phase = "design" +episode = 1 +hook = "安全なシステムは、みんな1枚の図から始まる。" +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", bg="board", alt="ホワイトボードのアーキテクチャ図を指すSec") }} + +{% bubble(who="sec", name="Sec") %}コードを書く前に聞かせて。ここで何が起こりうる?{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="sneak", bg="board", alt="すでに図の中に隠れているバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}シーッ。あのログインボックスに住むつもりだったのに。{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +設計は、修正のコストがインシデントレポートではなく鉛筆のひと筆で済むフェーズです。ソフトウェアとビジネスの設計は、最初のスケッチからセキュリティを考えるべきです。どのデータが重要か、誰が触るのか、攻撃者ならまずどこを狙うか。 + +このフェーズを支える習慣は2つ。**セキュア開発ライフサイクル**(Microsoft SDL、OWASP SAMM、BSIMM、NIST SSDF)は、リリースごとに繰り返せるセキュリティ活動のセットをチームに与えます。**脅威モデリング**は、アーキテクチャ図を囲んで半日、4つの質問を投げかけること。何を作っているのか、何が起こりうるのか、どう対処するのか、そしてうまくやれたか? + +{% caps() %} +- セキュアSDLC +- 脅威モデリング +{% endcaps %} + +## 実践では + +始めるのに特別なソフトは要りません。ホワイトボードと4つの質問で、かなり遠くまで行けます。モデルをコードの隣に置きたくなったら、コードで記述しましょう。これは[pytm](https://github.com/izar/pytm)。数行のPythonから図と脅威リストが生成されます。 + +```python +from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor + +tm = TM("Checkout service") +internet = Boundary("Internet") +user = Actor("Customer", inBoundary=internet) +web = Server("Web frontend") +db = Datastore("Orders DB") + +Dataflow(user, web, "Place order (HTTPS)") +Dataflow(web, db, "Store order (TLS, least privilege)") + +tm.process() +``` + +実行すると、データフロー図と、チームでレビューすべき脅威の候補リストが得られます。タイピングより描くほうが好きなら、[OWASP Threat Dragon](https://threatdragon.github.io)と[Threagile](https://threagile.io)が同じ役割を果たします。 + +{% alert(type="tip", title="シフトレフト") %}欠陥は早く見つけるほど安く直せます。設計は、行ける限りいちばん左です。{% endalert %} + +## さらに読む + +{% resources() %} +- [Microsoftセキュア開発ライフサイクル](https://www.microsoft.com/en-us/securityengineering/sdl/practices) +- [OWASP Software Assurance Maturity Model](https://github.com/OWASP/samm) +- [Building Security In Maturity Model (BSIMM)](https://www.bsimm.com/framework.html) +- [NIST Secure Software Development Framework](https://csrc.nist.gov/CSRC/media/Publications/white-paper/2019/06/07/mitigating-risk-of-software-vulnerabilities-with-ssdf/draft/documents/ssdf-for-mitigating-risk-of-software-vulns-draft.pdf) +- [DevSecOpsの基本: シフトレフトの9つのヒント (GitLab)](https://about.gitlab.com/blog/2020/06/23/efficient-devsecops-nine-tips-shift-left/) +- [OWASP脅威モデリング](https://owasp.org/www-community/Threat_Modeling) +- [OWASPアプリケーション脅威モデリング](https://owasp.org/www-community/Application_Threat_Modeling) +- [脅威モデリングとは (Wikipedia)](https://en.wikipedia.org/wiki/Threat_model) +{% endresources %} diff --git a/website/content/design/_index.ko.md b/website/content/design/_index.ko.md new file mode 100644 index 0000000..c7af393 --- /dev/null +++ b/website/content/design/_index.ko.md @@ -0,0 +1,75 @@ ++++ +title = "설계" +description = "위협 모델링과 시큐어 SDLC. 보안은 첫 커밋 전에 시작됩니다." +template = "chapter" +weight = 1 + +[extra] +phase = "design" +episode = 1 +hook = "안전한 시스템은 모두 그림에서 시작해요." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", bg="board", alt="화이트보드의 아키텍처 스케치를 가리키는 Sec") }} + +{% bubble(who="sec", name="Sec") %}코드를 쓰기 전에 먼저 물어봐요. 여기서 뭐가 잘못될 수 있을까요?{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="sneak", bg="board", alt="이미 다이어그램 속에 숨어 있는 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}쉿. 저 로그인 박스에 살려고 했는데.{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +설계는 수정 비용이 인시던트 리포트가 아니라 연필 한 획인 단계예요. 소프트웨어와 비즈니스 설계는 첫 스케치부터 보안을 고려해야 합니다. 어떤 데이터가 중요한지, 누가 만지는지, 공격자라면 무엇부터 노릴지를요. + +이 단계를 지탱하는 습관은 두 가지예요. **시큐어 개발 라이프사이클**(Microsoft SDL, OWASP SAMM, BSIMM, NIST SSDF)은 릴리스마다 반복할 수 있는 보안 활동 목록을 팀에 쥐여 줍니다. **위협 모델링**은 아키텍처 다이어그램을 놓고 오후 한나절 동안 네 가지 질문을 던지는 일이죠. 무엇을 만들고 있나, 무엇이 잘못될 수 있나, 그래서 어떻게 할 건가, 그리고 잘 해냈나? + +{% caps() %} +- 시큐어 SDLC +- 위협 모델링 +{% endcaps %} + +## 실전에서는 + +시작하는 데 특별한 도구는 필요 없어요. 화이트보드와 네 가지 질문이면 충분히 멀리 갑니다. 모델을 코드 옆에 두고 싶다면 코드로 기술하세요. 아래는 [pytm](https://github.com/izar/pytm)이에요. 파이썬 몇 줄로 다이어그램과 위협 목록이 만들어집니다. + +```python +from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor + +tm = TM("Checkout service") +internet = Boundary("Internet") +user = Actor("Customer", inBoundary=internet) +web = Server("Web frontend") +db = Datastore("Orders DB") + +Dataflow(user, web, "Place order (HTTPS)") +Dataflow(web, db, "Store order (TLS, least privilege)") + +tm.process() +``` + +실행하면 데이터 흐름 다이어그램과 함께 팀과 검토할 만한 위협 목록이 나와요. 타이핑보다 그리는 게 좋다면 [OWASP Threat Dragon](https://threatdragon.github.io)과 [Threagile](https://threagile.io)이 같은 역할을 해 줍니다. + +{% alert(type="tip", title="시프트 레프트") %}결함은 일찍 찾을수록 고치는 비용이 싸져요. 설계는 갈 수 있는 가장 왼쪽입니다.{% endalert %} + +## 더 읽어보기 + +{% resources() %} +- [Microsoft 시큐어 개발 라이프사이클](https://www.microsoft.com/en-us/securityengineering/sdl/practices) +- [OWASP Software Assurance Maturity Model](https://github.com/OWASP/samm) +- [Building Security In Maturity Model (BSIMM)](https://www.bsimm.com/framework.html) +- [NIST Secure Software Development Framework](https://csrc.nist.gov/CSRC/media/Publications/white-paper/2019/06/07/mitigating-risk-of-software-vulnerabilities-with-ssdf/draft/documents/ssdf-for-mitigating-risk-of-software-vulns-draft.pdf) +- [DevSecOps 기초: 시프트 레프트 팁 9가지 (GitLab)](https://about.gitlab.com/blog/2020/06/23/efficient-devsecops-nine-tips-shift-left/) +- [OWASP 위협 모델링](https://owasp.org/www-community/Threat_Modeling) +- [OWASP 애플리케이션 위협 모델링](https://owasp.org/www-community/Application_Threat_Modeling) +- [위협 모델링이란 (Wikipedia)](https://en.wikipedia.org/wiki/Threat_model) +{% endresources %} diff --git a/website/content/design/_index.md b/website/content/design/_index.md new file mode 100644 index 0000000..457e7fb --- /dev/null +++ b/website/content/design/_index.md @@ -0,0 +1,75 @@ ++++ +title = "Design" +description = "Threat modeling and a secure SDLC: security starts before the first commit." +template = "chapter" +weight = 1 + +[extra] +phase = "design" +episode = 1 +hook = "Every safe system starts as a drawing." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", bg="board", alt="Sec points at the architecture sketch on the whiteboard") }} + +{% bubble(who="sec", name="Sec") %}Before we write a line of code: what could go wrong here?{% endbubble %} +{% endpanel %} + +{% panel(tint="warm") %} +{{ scene(name="char-bug", pose="sneak", bg="board", alt="The Bug is already hiding inside the diagram") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}Shh. I was planning to live in that login box.{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +Design is where a fix costs a pencil stroke instead of an incident report. The software and business design should consider security from the very first sketch: which data matters, who touches it, and what an attacker would try first. + +Two habits carry this phase. A **secure development lifecycle** (Microsoft SDL, OWASP SAMM, BSIMM, NIST SSDF) gives the team a repeatable set of security activities for every release. **Threat modeling** takes one afternoon with the architecture diagram and asks four questions: what are we building, what can go wrong, what will we do about it, and did we do a good job? + +{% caps() %} +- Secure SDLC +- Threat Modeling +{% endcaps %} + +## In practice + +You do not need special software to start: a whiteboard and the four questions go a long way. When you want the model to live next to the code, describe it as code. This is [pytm](https://github.com/izar/pytm), where the diagram and the threat list are generated from a few lines of Python: + +```python +from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor + +tm = TM("Checkout service") +internet = Boundary("Internet") +user = Actor("Customer", inBoundary=internet) +web = Server("Web frontend") +db = Datastore("Orders DB") + +Dataflow(user, web, "Place order (HTTPS)") +Dataflow(web, db, "Store order (TLS, least privilege)") + +tm.process() +``` + +Run it and you get a data flow diagram plus a list of likely threats to review with the team. Prefer drawing over typing? [OWASP Threat Dragon](https://threatdragon.github.io) and [Threagile](https://threagile.io) cover the same ground. + +{% alert(type="tip", title="Shift left") %}The earlier a flaw is found, the cheaper it is to fix. Design is as far left as it gets.{% endalert %} + +## Keep reading + +{% resources() %} +- [Microsoft Secure Development Lifecycle](https://www.microsoft.com/en-us/securityengineering/sdl/practices) +- [OWASP Software Assurance Maturity Model](https://github.com/OWASP/samm) +- [Building Security In Maturity Model (BSIMM)](https://www.bsimm.com/framework.html) +- [NIST Secure Software Development Framework](https://csrc.nist.gov/CSRC/media/Publications/white-paper/2019/06/07/mitigating-risk-of-software-vulnerabilities-with-ssdf/draft/documents/ssdf-for-mitigating-risk-of-software-vulns-draft.pdf) +- [DevSecOps basics: 9 tips for shifting left (GitLab)](https://about.gitlab.com/blog/2020/06/23/efficient-devsecops-nine-tips-shift-left/) +- [Threat Modeling by OWASP](https://owasp.org/www-community/Threat_Modeling) +- [Application Threat Modeling by OWASP](https://owasp.org/www-community/Application_Threat_Modeling) +- [What is Threat Modeling (Wikipedia)](https://en.wikipedia.org/wiki/Threat_model) +{% endresources %} diff --git a/website/content/develop/_index.ja.md b/website/content/develop/_index.ja.md new file mode 100644 index 0000000..9342f15 --- /dev/null +++ b/website/content/develop/_index.ja.md @@ -0,0 +1,66 @@ ++++ +title = "開発" +description = "セキュアコーディングの習慣、コード署名、リポジトリのアクセス制御。" +template = "chapter" +weight = 2 + +[extra] +phase = "develop" +episode = 2 +hook = "良い習慣は、英雄的な火消しに勝る。" +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="ユーザー入力をそのままクエリに貼り付けるDev") }} + +{% bubble(who="dev", name="Dev") %}ユーザー入力がそのままクエリへ。たぶん大丈夫?{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="calm", alt="より安全なパターンを教えるSec") }} + +{% bubble(who="sec", dir="right", name="Sec") %}パラメータ化して。バグは文字列連結が大好物だから。{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +ほとんどの脆弱性は、デプロイされたときではなく、タイプされたときに生まれます。開発フェーズの目標は、安全なやり方をいちばん楽なやり方にすること。使っている言語の**セキュアコーディングガイド**に従い、レビューは罠探しではなくロジックに集中させましょう。 + +地味ながら同じくらい大事な習慣が2つ。**コード認証**はコミットに署名して、履歴に本当の作者を語らせること。**リポジトリアクセス制御**は最小権限と保護ブランチで、ノートPCが1台漏れても`main`を書き換えられないようにすることです。 + +{% caps() %} +- セキュアコーディング +- コード認証 +- リポジトリアクセス制御 +{% endcaps %} + +## 実践では + +リポジトリに作者の真実を語らせ、危険なプッシュを拒否させましょう。 + +```bash +# すべてのコミットに署名して、履歴に本当の作者を語らせる +git config --global commit.gpgsign true + +# デフォルトブランチを保護: レビュー必須、強制プッシュ禁止 +gh api -X PUT repos/hahwul/shop/branches/main/protection \ + -F required_pull_request_reviews[required_approving_review_count]=1 \ + -F enforce_admins=true -F allow_force_pushes=false +``` + +あとはスタックに合ったセキュアコーディングガイドを選んで、レビュー中はタブ1つ分の距離に置いておくこと。AppleからRailsまで、定番は下にリンクしてあります。 + +## さらに読む + +{% resources() %} +- [Appleセキュアコーディングガイド](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Introduction.html) +- [Java SEセキュアコーディングガイドライン](https://www.oracle.com/java/technologies/javase/seccodeguide.html) +- [Go-SCP: Goセキュアコーディングプラクティス](https://github.com/OWASP/Go-SCP) +- [GoogleによるAndroidアプリセキュリティのベストプラクティス](https://developer.android.com/topic/security/best-practices) +- [Securing Rails Applications](https://guides.rubyonrails.org/security.html) +{% endresources %} diff --git a/website/content/develop/_index.ko.md b/website/content/develop/_index.ko.md new file mode 100644 index 0000000..2d7720b --- /dev/null +++ b/website/content/develop/_index.ko.md @@ -0,0 +1,66 @@ ++++ +title = "개발" +description = "시큐어 코딩 습관, 코드 서명, 저장소 접근 제어." +template = "chapter" +weight = 2 + +[extra] +phase = "develop" +episode = 2 +hook = "좋은 습관이 영웅적인 수습보다 나아요." +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="사용자 입력을 그대로 쿼리에 붙여 넣는 Dev") }} + +{% bubble(who="dev", name="Dev") %}사용자 입력이 쿼리로 바로 들어가네요. 괜찮겠죠?{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="calm", alt="더 안전한 패턴을 알려 주는 Sec") }} + +{% bubble(who="sec", dir="right", name="Sec") %}파라미터로 바인딩하세요. 버그는 문자열 이어 붙이기를 사랑하거든요.{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +대부분의 취약점은 배포될 때가 아니라 타이핑될 때 태어나요. 개발 단계의 목표는 안전한 길을 곧 가장 편한 길로 만드는 것입니다. 사용하는 언어의 **시큐어 코딩 가이드**를 따르고, 리뷰는 함정 찾기 대신 로직에 집중하게 하세요. + +조용하지만 똑같이 중요한 습관이 두 가지 더 있어요. **코드 인증**은 커밋에 서명해서 히스토리가 진짜 작성자를 말하게 하는 것. **저장소 접근 제어**는 최소 권한과 보호 브랜치로, 노트북 한 대가 털려도 `main`을 다시 쓸 수 없게 하는 것이죠. + +{% caps() %} +- 시큐어 코딩 +- 코드 인증 +- 저장소 접근 제어 +{% endcaps %} + +## 실전에서는 + +저장소가 작성자에 대해 진실을 말하게 하고, 위험한 푸시는 거절하게 만드세요. + +```bash +# 모든 커밋에 서명해서, 히스토리가 진짜 작성자를 말하게 하기 +git config --global commit.gpgsign true + +# 기본 브랜치 보호: 리뷰 필수, 강제 푸시 차단 +gh api -X PUT repos/hahwul/shop/branches/main/protection \ + -F required_pull_request_reviews[required_approving_review_count]=1 \ + -F enforce_admins=true -F allow_force_pushes=false +``` + +그다음 스택에 맞는 시큐어 코딩 가이드를 골라 리뷰할 때 한 탭 거리에 두세요. Apple부터 Rails까지, 고전들은 아래에 있습니다. + +## 더 읽어보기 + +{% resources() %} +- [Apple 시큐어 코딩 가이드](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Introduction.html) +- [Java SE 시큐어 코딩 가이드라인](https://www.oracle.com/java/technologies/javase/seccodeguide.html) +- [Go-SCP: Go 시큐어 코딩 프랙티스](https://github.com/OWASP/Go-SCP) +- [Google의 Android 앱 보안 모범 사례](https://developer.android.com/topic/security/best-practices) +- [Securing Rails Applications](https://guides.rubyonrails.org/security.html) +{% endresources %} diff --git a/website/content/develop/_index.md b/website/content/develop/_index.md new file mode 100644 index 0000000..bbc1979 --- /dev/null +++ b/website/content/develop/_index.md @@ -0,0 +1,66 @@ ++++ +title = "Develop" +description = "Secure coding habits, signed code, and controlled access to the repository." +template = "chapter" +weight = 2 + +[extra] +phase = "develop" +episode = 2 +hook = "Good habits beat heroic fixes." +has_tools = false ++++ + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="Dev pastes user input straight into a query") }} + +{% bubble(who="dev", name="Dev") %}User input goes straight into the query. Probably fine?{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="calm", alt="Sec suggests the safer pattern") }} + +{% bubble(who="sec", dir="right", name="Sec") %}Parameterize it. The Bug loves string concatenation.{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +Most vulnerabilities are typed, not deployed. The Develop phase is about making the safe way the easy way: follow the **secure coding guide** for your language, and let reviews focus on logic instead of footguns. + +Two quieter habits matter just as much. **Code authentication** means signed commits, so the history says who really wrote what. **Repository access control** means least privilege and protected branches, so one leaked laptop cannot rewrite `main`. + +{% caps() %} +- Secure Coding +- Code Authentication +- Repository Access Control +{% endcaps %} + +## In practice + +Make the repository tell the truth about its authors and refuse risky pushes: + +```bash +# Sign every commit, so the history says who really wrote it +git config --global commit.gpgsign true + +# Protect the default branch: reviews required, force pushes blocked +gh api -X PUT repos/hahwul/shop/branches/main/protection \ + -F required_pull_request_reviews[required_approving_review_count]=1 \ + -F enforce_admins=true -F allow_force_pushes=false +``` + +Then pick the secure coding guide that matches your stack and keep it one tab away during review. The classics are linked below, from Apple to Rails. + +## Keep reading + +{% resources() %} +- [Secure Coding Guide by Apple](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Introduction.html) +- [Secure Coding Guidelines for Java SE](https://www.oracle.com/java/technologies/javase/seccodeguide.html) +- [Go-SCP: Go Secure Coding Practices](https://github.com/OWASP/Go-SCP) +- [Android App Security Best Practices by Google](https://developer.android.com/topic/security/best-practices) +- [Securing Rails Applications](https://guides.rubyonrails.org/security.html) +{% endresources %} diff --git a/website/content/index.ja.md b/website/content/index.ja.md new file mode 100644 index 0000000..e99d263 --- /dev/null +++ b/website/content/index.ja.md @@ -0,0 +1,75 @@ ++++ +title = "マンガで学ぶDevSecOps" +description = "設計、開発、ビルド、テスト、デプロイ、運用。6つのイラストエピソードでDevSecOpsを学び、合わせて使えるツールにも出会えます。" +template = "home" + +[extra] +hero_title = "速くリリース。
それでもセキュアに。" +hero_sub = "マンガで学ぶDevSecOpsフィールドガイド。ループを巡る6つのエピソードと、それに合うツールたち。" ++++ + +## プロローグ: バグがリリースされた日 + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="ノートPCの前で楽しそうにタイピングするDev") }} + +{% bubble(who="dev", name="Dev") %}機能できた!今日リリースしちゃおう!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="sneak", alt="誰にも気づかれずリリースに忍び込むバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}このリリース、あと1匹入れます?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", alt="3週間遅れで欠陥を見つけたSec") }} + +{% bubble(who="sec", name="Sec") %}見つけました。本番環境で。3週間も経ってから。{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="ダッシュボードを見ながらひらめくOps") }} + +{% bubble(who="ops", name="Ops") %}セキュリティが1ページ目から一緒だったら?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +これこそがDevSecOpsの考え方です。セキュリティを最後の検査として迎えるのではなく、開発・セキュリティ・運用がひとつのチームとなって、ライフサイクルの全フェーズを一緒に進みます。 + +## 1つのループ、6つのエピソード + +セキュリティは最後の関門ではありません。最初のスケッチから最後のアラートまで、ループ全体を一緒に走ります。 + +{{ loop_diagram(caption="各フェーズが1つのエピソード。クリックして読み始めましょう。") }} + +## チーム紹介 + +{% team() %} + +{% panel() %} +{% member(char="char-dev", pose="wave", title="Dev") %}作るのが速く、リリースが大好き。スピードと安全は敵ではなく仲間だと学んでいきます。{% endmember %} + +{% member(char="char-sec", pose="calm", title="Sec") %}冷静で目が鋭い。本番でパッチを当てるより、ホワイトボードの上で欠陥を消すほうが好きです。{% endmember %} + +{% member(char="char-ops", pose="monitor", title="Ops") %}ダッシュボードを守る頼れる存在。明かりはつけたまま、ドアには鍵を、ポケベルは静かに。{% endmember %} +{% endpanel %} + +{% panel(tint="warm", halftone=true) %} +{% member(char="char-bug", pose="sneak", title="バグ") %}急いで書いたコードや忘れられた設定に忍び込みます。悪役というより日和見主義。エピソードを追うごとに、チームがバグを早く捕まえていく様子をどうぞ。{% endmember %} +{% endpanel %} + +{% endteam %} + +## エピソード一覧 + +{{ episode_rail() }} + +{{ finale(title="ループは終わらない。物語も続く。", alt="小さな白旗を振るバグ") }} diff --git a/website/content/index.ko.md b/website/content/index.ko.md new file mode 100644 index 0000000..7430d33 --- /dev/null +++ b/website/content/index.ko.md @@ -0,0 +1,75 @@ ++++ +title = "만화로 배우는 DevSecOps" +description = "설계, 개발, 빌드, 테스트, 배포, 운영. 여섯 개의 일러스트 에피소드로 DevSecOps를 배우고, 어울리는 도구까지 함께 만나 보세요." +template = "home" + +[extra] +hero_title = "빠르게 배포해도,
보안은 단단하게." +hero_sub = "만화로 배우는 DevSecOps 필드 가이드. 루프를 도는 여섯 개의 에피소드와 어울리는 도구들." ++++ + +## 프롤로그: 버그가 배포되던 날 + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="노트북 앞에서 신나게 타이핑하는 Dev") }} + +{% bubble(who="dev", name="Dev") %}기능 완성! 오늘 바로 배포할 거예요!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="sneak", alt="아무도 모르게 릴리스에 숨어드는 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}이번 릴리스에 한 자리 남았나요?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", alt="3주 늦게 결함을 찾아낸 Sec") }} + +{% bubble(who="sec", name="Sec") %}찾았어요. 운영 환경에서요. 3주나 지나서요.{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="대시보드를 보다가 아이디어를 떠올린 Ops") }} + +{% bubble(who="ops", name="Ops") %}보안이 1페이지부터 함께했다면 어땠을까요?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +이게 바로 DevSecOps의 핵심이에요. 보안이 마지막 검수로 등장하는 대신, 개발과 보안과 운영이 한 팀이 되어 라이프사이클의 모든 단계를 함께 만들어 갑니다. + +## 하나의 루프, 여섯 개의 에피소드 + +보안은 마지막 관문이 아니에요. 첫 스케치부터 마지막 알림까지, 루프 전체를 함께 달립니다. + +{{ loop_diagram(caption="각 단계가 하나의 에피소드예요. 눌러서 읽어 보세요.") }} + +## 팀을 소개합니다 + +{% team() %} + +{% panel() %} +{% member(char="char-dev", pose="wave", title="Dev") %}빠르게 만들고 배포를 사랑해요. 속도와 안전이 라이벌이 아니라 한 팀이라는 걸 배워 갑니다.{% endmember %} + +{% member(char="char-sec", pose="calm", title="Sec") %}차분하고 눈이 날카로워요. 운영 환경에서 패치하는 것보다 화이트보드에서 결함을 지우는 쪽을 좋아하죠.{% endmember %} + +{% member(char="char-ops", pose="monitor", title="Ops") %}대시보드를 지키는 든든한 손. 불은 켜 두고, 문은 잠그고, 호출기는 조용하게 유지합니다.{% endmember %} +{% endpanel %} + +{% panel(tint="warm", halftone=true) %} +{% member(char="char-bug", pose="sneak", title="버그") %}서두른 코드와 잊힌 설정에 숨어들어요. 악당이라기보다는 기회주의자죠. 에피소드마다 팀이 버그를 점점 더 일찍 잡는 걸 지켜보세요.{% endmember %} +{% endpanel %} + +{% endteam %} + +## 에피소드 + +{{ episode_rail() }} + +{{ finale(title="루프는 끝나지 않아요. 이야기도 계속됩니다.", alt="작은 흰 깃발을 흔드는 버그") }} diff --git a/website/content/index.md b/website/content/index.md new file mode 100644 index 0000000..4ca6fc8 --- /dev/null +++ b/website/content/index.md @@ -0,0 +1,75 @@ ++++ +title = "DevSecOps, the comic" +description = "Learn DevSecOps through six illustrated episodes: Design, Develop, Build, Test, Deploy, and Operate, with the tools to match." +template = "home" + +[extra] +hero_title = "Ship fast.
Stay secure." +hero_sub = "A comic field guide to DevSecOps: six episodes around the loop, and the tools to match." ++++ + +## Prologue: the day the Bug shipped + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-dev", pose="typing", bg="window", alt="Dev types happily on a little laptop") }} + +{% bubble(who="dev", name="Dev") %}Feature's done. Shipping it today!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="sneak", alt="The Bug slips into the release unnoticed") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}Room for one more in this release?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +{% strip(row=true) %} + +{% panel() %} +{{ scene(name="char-sec", pose="point", alt="Sec points at a finding, three weeks too late") }} + +{% bubble(who="sec", name="Sec") %}Found it. In production. Three weeks later.{% endbubble %} +{% endpanel %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="Ops looks up from the dashboards with an idea") }} + +{% bubble(who="ops", name="Ops") %}What if security joined the story on page one?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +That is the whole idea behind DevSecOps: development, security, and operations working as one team through every phase of the lifecycle, instead of security arriving as a final inspection. + +## One loop, six episodes + +Security is not a gate at the end. It rides along the entire loop, from the first sketch to the last alert. + +{{ loop_diagram(caption="Every phase is an episode. Click one to start reading.") }} + +## Meet the team + +{% team() %} + +{% panel() %} +{% member(char="char-dev", pose="wave", title="Dev") %}Builds fast and loves shipping. Learns along the way that speed and safety are teammates, not rivals.{% endmember %} + +{% member(char="char-sec", pose="calm", title="Sec") %}Calm and sharp-eyed. Would much rather erase a flaw from a whiteboard than patch it in production.{% endmember %} + +{% member(char="char-ops", pose="monitor", title="Ops") %}Steady hands on the dashboards. Keeps the lights on, the doors locked, and the pager quiet.{% endmember %} +{% endpanel %} + +{% panel(tint="warm", halftone=true) %} +{% member(char="char-bug", pose="sneak", title="The Bug") %}Sneaks into rushed code and forgotten configs. Not evil, just opportunistic. Watch the team catch it earlier in every episode.{% endmember %} +{% endpanel %} + +{% endteam %} + +## The episodes + +{{ episode_rail() }} + +{{ finale(title="The loop never ends. Neither does the story.", alt="The Bug waves a little white flag") }} diff --git a/website/content/operate/_index.ja.md b/website/content/operate/_index.ja.md new file mode 100644 index 0000000..704c3e6 --- /dev/null +++ b/website/content/operate/_index.ja.md @@ -0,0 +1,66 @@ ++++ +title = "運用" +description = "RASP、監査、監視、パッチ適用。リリースは見張りの始まりです。" +template = "chapter" +weight = 6 + +[extra] +phase = "operate" +episode = 6 +hook = "リリースは、見張りの始まり。" +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="緑のダッシュボードと武装済みのアラートを見守るOps") }} + +{% bubble(who="ops", name="Ops") %}ダッシュボードは緑。アラート武装完了。RASPも勤務中。{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="数秒でアラートに引っかかったバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}依存関係を1つ触っただけなのに!なんで数秒でアラートが鳴るの?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +本番環境は、物語が続いていく場所です。**RASP**はアプリケーションの中に乗り込んで、実行時に攻撃をブロックします。**監視**はログとメトリクスを、誰かが実際に見るアラートへ変え、定期的な**監査**は、統制がまだ現実と合っているかを問いかけます。 + +そして**パッチ適用**。セキュリティでいちばん地味な超能力です。新しいCVEは毎日届きます。勝つチームは、単純に速くアップデートするチーム。何がどこで動いているかを知るコンポーネント分析が、それを支えます。 + +{% caps() %} +- RASP +- 監査 +- 監視 +- パッチ適用 +{% endcaps %} + +## 実践では + +リリースのときだけでなく、スケジュールに乗せて問い続けましょう。 + +```bash +# 毎晩: CIS基準でクラウドアカウントを監査 +prowler aws --compliance cis_2.0_aws + +# 毎晩: 新しく公開されたCVEに備えてライブイメージを再スキャン +trivy image --scanners vuln registry.example.com/shop:live +``` + +SBOMをDependency-Trackのようなコンポーネント分析プラットフォームに流しておけば、新しいCVEが落ちた朝、「うちで動いているものに影響は?」という質問がひとりでに答えを持ってきます。 + +{% alert(type="tip", title="ループを閉じよう") %}運用で学んだこと(攻撃パターン、うるさいアラート、知らなかった依存関係)は、次のエピソードの設計インプットになります。だからループなのです。{% endalert %} + +## さらに読む + +{% resources() %} +- [Runtime Application Self-Protection (Rapid7)](https://www.rapid7.com/fundamentals/runtime-application-self-protection/) +- [IASTとRASPで始めるDevSecOpsパイプライン](https://2018.appsec.eu/presos/DevOps_Jumpstarting-Your-DevSecOps_Jeff-Williams_AppSecEU2018.pdf) +- [OWASP DSOVSが定義するIASTレベル](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-003-Interactive-Application-Security-Testing-IAST.md) +- [攻撃対象面分析チートシート (OWASP)](https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html) +{% endresources %} diff --git a/website/content/operate/_index.ko.md b/website/content/operate/_index.ko.md new file mode 100644 index 0000000..a13546f --- /dev/null +++ b/website/content/operate/_index.ko.md @@ -0,0 +1,66 @@ ++++ +title = "운영" +description = "RASP, 감사, 모니터링, 패치. 배포는 지켜보기의 시작입니다." +template = "chapter" +weight = 6 + +[extra] +phase = "operate" +episode = 6 +hook = "배포는 지켜보기의 시작이에요." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="초록색 대시보드와 무장된 알림을 지켜보는 Ops") }} + +{% bubble(who="ops", name="Ops") %}대시보드 초록불. 알림 무장 완료. RASP 근무 중.{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="몇 초 만에 알림에 걸린 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}의존성 하나 건드렸을 뿐인데! 알림이 어떻게 몇 초 만에 울려요?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +운영 환경은 이야기가 계속되는 곳이에요. **RASP**는 애플리케이션 안에 타고 들어가 런타임에 공격을 막습니다. **모니터링**은 로그와 메트릭을 누군가 실제로 보는 알림으로 바꾸고, 정기적인 **감사**는 통제 장치가 여전히 현실과 맞는지 물어요. + +그리고 **패치**가 있죠. 보안에서 제일 화려하지 않은 초능력이요. 새 CVE는 매일 쏟아지고, 이기는 팀은 그저 빨리 업데이트하는 팀입니다. 무엇이 어디서 돌고 있는지 아는 컴포넌트 분석이 그걸 도와줘요. + +{% caps() %} +- RASP +- 감사 +- 모니터링 +- 패치 +{% endcaps %} + +## 실전에서는 + +릴리스 때만이 아니라, 일정에 맞춰 계속 질문을 던지세요. + +```bash +# 매일 밤: CIS 기준으로 클라우드 계정 감사 +prowler aws --compliance cis_2.0_aws + +# 매일 밤: 새로 공개된 CVE에 대해 라이브 이미지 재스캔 +trivy image --scanners vuln registry.example.com/shop:live +``` + +SBOM을 Dependency-Track 같은 컴포넌트 분석 플랫폼에 흘려 두면, 새 CVE가 떨어진 아침에 "우리가 돌리는 것 중에 영향받는 게 있나?"라는 질문이 저절로 답을 얻습니다. + +{% alert(type="tip", title="루프를 닫으세요") %}운영하며 배운 것들(공격 패턴, 시끄러운 알림, 몰랐던 의존성)이 다음 에피소드의 설계 입력이 돼요. 그래서 루프인 거죠.{% endalert %} + +## 더 읽어보기 + +{% resources() %} +- [Runtime Application Self-Protection (Rapid7)](https://www.rapid7.com/fundamentals/runtime-application-self-protection/) +- [IAST와 RASP로 시작하는 DevSecOps 파이프라인](https://2018.appsec.eu/presos/DevOps_Jumpstarting-Your-DevSecOps_Jeff-Williams_AppSecEU2018.pdf) +- [OWASP DSOVS가 정의한 IAST 레벨](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-003-Interactive-Application-Security-Testing-IAST.md) +- [공격 표면 분석 치트 시트 (OWASP)](https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html) +{% endresources %} diff --git a/website/content/operate/_index.md b/website/content/operate/_index.md new file mode 100644 index 0000000..3f27acc --- /dev/null +++ b/website/content/operate/_index.md @@ -0,0 +1,66 @@ ++++ +title = "Operate" +description = "RASP, audits, monitoring, and patching: shipping is the beginning of the watch." +template = "chapter" +weight = 6 + +[extra] +phase = "operate" +episode = 6 +hook = "Shipping is the beginning of the watch." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-ops", pose="monitor", bg="radar", alt="Ops watches green dashboards with alerts armed") }} + +{% bubble(who="ops", name="Ops") %}Dashboards green. Alerts armed. RASP on duty.{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="The Bug trips an alert within seconds") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}I touched one dependency! How did the alert fire in seconds?{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +Production is where the story keeps going. **RASP** rides inside the application and blocks attacks at runtime. **Monitoring** turns logs and metrics into alerts someone actually sees, and regular **audits** ask whether the controls still match reality. + +Then there is **patching**, the least glamorous superpower in security. New CVEs land every day; the teams that win are simply the ones that update fast, helped by component analysis that knows what is running where. + +{% caps() %} +- RASP +- Audit +- Monitoring +- Patching +{% endcaps %} + +## In practice + +Keep asking questions on a schedule, not just at release time: + +```bash +# Nightly: audit the cloud account against CIS +prowler aws --compliance cis_2.0_aws + +# Nightly: rescan the live image for newly published CVEs +trivy image --scanners vuln registry.example.com/shop:live +``` + +Feed your SBOMs to a component-analysis platform like Dependency-Track and the "is anything we run affected?" question answers itself the morning a new CVE drops. + +{% alert(type="tip", title="Close the loop") %}What you learn while operating (attack patterns, noisy alerts, surprise dependencies) is next episode's design input. That is why it is a loop.{% endalert %} + +## Keep reading + +{% resources() %} +- [Runtime Application Self-Protection (Rapid7)](https://www.rapid7.com/fundamentals/runtime-application-self-protection/) +- [Jumpstarting Your DevSecOps Pipeline with IAST and RASP](https://2018.appsec.eu/presos/DevOps_Jumpstarting-Your-DevSecOps_Jeff-Williams_AppSecEU2018.pdf) +- [IAST Levels Defined by OWASP DSOVS](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-003-Interactive-Application-Security-Testing-IAST.md) +- [Attack Surface Analysis Cheat Sheet (OWASP)](https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html) +{% endresources %} diff --git a/website/content/test/_index.ja.md b/website/content/test/_index.ja.md new file mode 100644 index 0000000..67bc9f0 --- /dev/null +++ b/website/content/test/_index.ja.md @@ -0,0 +1,72 @@ ++++ +title = "テスト" +description = "動いているアプリへのDASTとペンテスト。誰かにやられる前に、自分たちで。" +template = "chapter" +weight = 4 + +[extra] +phase = "test" +episode = 4 +hook = "誰かに攻撃される前に、自分を攻撃しよう。" +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="happy", bg="window", alt="楽しそうにステージングへの攻撃を始めるSec") }} + +{% bubble(who="sec", name="Sec") %}ステージングが起動しました。自分たちのアプリを攻撃する時間です!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="お気に入りの隠れ家から追い出されたバグ") }} + +{% bubble(who="bug", dir="right", name="バグ") %}ログインページをスキャンしたの?あそこ、僕のお気に入りのドアだったのに!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## このフェーズで起こること + +静的解析はコードを読みますが、このフェーズは動いているものをつつきます。**DAST**はデプロイ済みのアプリに本物のリクエストを送り、どう誤動作するかを観察します。ブラウザを持った攻撃者とまったく同じやり方で。**IAST**はテストが走る間、プロセスの内側から見張ります。 + +自動化がカバーするのは既知のパターン。残りは**ペネトレーションテスト**が持つ人間の創造力の出番です。連鎖するロジックの欠陥、妙な隅っこ、どのスキャナーにもテンプレートがないもの。本番の前に自動と手動のテストを両方回す習慣が効きます。 + +{% caps() %} +- DAST +- IAST +- ペンテスト +{% endcaps %} + +## 実践では + +CIからそのまま動く、毎週月曜のステージング向けZAPベースラインスキャンです。 + +```yaml +name: dast +on: + schedule: + - cron: "0 3 * * 1" + +jobs: + zap: + runs-on: ubuntu-latest + steps: + - name: ZAP baseline scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: https://staging.example.com +``` + +ベースラインスキャンはパッシブなので、頻繁に回しても安全です。アクティブ攻撃を含むフルスキャンは自分の環境で、今週の新しい脆弱性はNucleiのようなテンプレートベースのスキャナーで、と段階を上げていきましょう。 + +## さらに読む + +{% resources() %} +- [ZAPとGitHub ActionsでDAST](https://www.zaproxy.org/blog/2020-05-15-dynamic-application-security-testing-with-zap-and-github-actions/) +- [GitLabのDAST](https://docs.gitlab.com/ee/user/application_security/dast/) +- [NucleiでDAST (GitHub Action)](https://github.com/secopslab/nuclei-action) +- [ZAPCon 2021: ZAPの民主化](https://www.youtube.com/watch?v=jimW-R6_F4U) +- [OWASP DSOVSが定義するDASTレベル](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-002-Dynamic-Application-Security-Testing-DAST.md) +{% endresources %} diff --git a/website/content/test/_index.ko.md b/website/content/test/_index.ko.md new file mode 100644 index 0000000..e78ce09 --- /dev/null +++ b/website/content/test/_index.ko.md @@ -0,0 +1,72 @@ ++++ +title = "테스트" +description = "실행 중인 앱을 겨냥한 DAST와 모의해킹. 누군가 하기 전에 우리가 먼저." +template = "chapter" +weight = 4 + +[extra] +phase = "test" +episode = 4 +hook = "누군가 공격하기 전에, 스스로를 공격하세요." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="happy", bg="window", alt="신나서 스테이징 공격을 시작하는 Sec") }} + +{% bubble(who="sec", name="Sec") %}스테이징 떴어요. 우리 앱, 우리가 먼저 공격할 시간!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="제일 아끼던 은신처에서 쫓겨난 버그") }} + +{% bubble(who="bug", dir="right", name="버그") %}로그인 페이지를 스캔했다고요? 거기 내가 제일 아끼는 문이었는데!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## 이 단계에서 벌어지는 일 + +정적 분석이 코드를 읽는다면, 이 단계는 실제로 돌아가는 것을 찔러 봐요. **DAST**는 배포된 앱에 진짜 요청을 보내고 어떻게 잘못 반응하는지 지켜봅니다. 브라우저를 든 공격자와 똑같이요. **IAST**는 테스트가 도는 동안 프로세스 안쪽에서 지켜봅니다. + +자동화는 알려진 패턴을 커버해요. 나머지는 **모의해킹**이 가진 인간의 창의력이 채웁니다. 연결된 로직 결함, 이상한 구석, 어떤 스캐너에도 템플릿이 없는 것들요. 운영 배포 전에 자동과 수동 테스트를 함께 돌리는 습관이 남는 장사입니다. + +{% caps() %} +- DAST +- IAST +- 모의해킹 +{% endcaps %} + +## 실전에서는 + +CI에서 바로 도는, 매주 월요일 스테이징 대상 ZAP 베이스라인 스캔이에요. + +```yaml +name: dast +on: + schedule: + - cron: "0 3 * * 1" + +jobs: + zap: + runs-on: ubuntu-latest + steps: + - name: ZAP baseline scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: https://staging.example.com +``` + +베이스라인 스캔은 수동적이라 자주 돌려도 안전해요. 능동 공격이 포함된 풀 스캔은 직접 소유한 환경에서, 그 주의 신규 취약점은 Nuclei 같은 템플릿 기반 스캐너로 졸업해 가세요. + +## 더 읽어보기 + +{% resources() %} +- [ZAP과 GitHub Actions로 하는 DAST](https://www.zaproxy.org/blog/2020-05-15-dynamic-application-security-testing-with-zap-and-github-actions/) +- [GitLab의 DAST](https://docs.gitlab.com/ee/user/application_security/dast/) +- [Nuclei로 하는 DAST (GitHub Action)](https://github.com/secopslab/nuclei-action) +- [ZAPCon 2021: ZAP의 대중화](https://www.youtube.com/watch?v=jimW-R6_F4U) +- [OWASP DSOVS가 정의한 DAST 레벨](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-002-Dynamic-Application-Security-Testing-DAST.md) +{% endresources %} diff --git a/website/content/test/_index.md b/website/content/test/_index.md new file mode 100644 index 0000000..0808305 --- /dev/null +++ b/website/content/test/_index.md @@ -0,0 +1,72 @@ ++++ +title = "Test" +description = "DAST and penetration testing against the running app, before anyone else gets to." +template = "chapter" +weight = 4 + +[extra] +phase = "test" +episode = 4 +hook = "Attack yourself before someone else does." +has_tools = true ++++ + +{% strip(row=true) %} + +{% panel(tint="sec") %} +{{ scene(name="char-sec", pose="happy", bg="window", alt="Sec cheerfully starts attacking the staging environment") }} + +{% bubble(who="sec", name="Sec") %}Staging is up. Time to attack our own app!{% endbubble %} +{% endpanel %} + +{% panel() %} +{{ scene(name="char-bug", pose="caught", alt="The Bug is flushed out of its favorite hiding spot") }} + +{% bubble(who="bug", dir="right", name="The Bug") %}You scanned the login page? That was my favorite door!{% endbubble %} +{% endpanel %} + +{% endstrip %} + +## What happens here + +Static analysis reads the code; this phase pokes the running thing. **DAST** sends real requests to a deployed app and watches how it misbehaves, exactly like an attacker with a browser would. **IAST** watches from inside the process while your tests run. + +Automation covers the known patterns. **Penetration testing** brings human creativity for the rest: chained logic flaws, odd corners, the things no scanner has a template for. Automatic and manual testing before production is the habit worth keeping. + +{% caps() %} +- DAST +- IAST +- Pentest +{% endcaps %} + +## In practice + +A weekly ZAP baseline scan against staging, straight from CI: + +```yaml +name: dast +on: + schedule: + - cron: "0 3 * * 1" + +jobs: + zap: + runs-on: ubuntu-latest + steps: + - name: ZAP baseline scan + uses: zaproxy/action-baseline@v0.14.0 + with: + target: https://staging.example.com +``` + +The baseline scan is passive and safe to run often. Graduate to the full scan (active attacks) on environments you own, and template-based scanners like Nuclei for the vulnerabilities of the week. + +## Keep reading + +{% resources() %} +- [DAST with ZAP and GitHub Actions](https://www.zaproxy.org/blog/2020-05-15-dynamic-application-security-testing-with-zap-and-github-actions/) +- [DAST in GitLab](https://docs.gitlab.com/ee/user/application_security/dast/) +- [DAST with Nuclei (GitHub Action)](https://github.com/secopslab/nuclei-action) +- [ZAPCon 2021: Democratizing ZAP](https://www.youtube.com/watch?v=jimW-R6_F4U) +- [DAST Levels Defined by OWASP DSOVS](https://github.com/OWASP/www-project-devsecops-verification-standard/blob/main/document/TEST-002-Dynamic-Application-Security-Testing-DAST.md) +{% endresources %} diff --git a/website/content/tools/_index.ja.md b/website/content/tools/_index.ja.md new file mode 100644 index 0000000..8ad8843 --- /dev/null +++ b/website/content/tools/_index.ja.md @@ -0,0 +1,7 @@ ++++ +title = "ツール" +description = "ループのフェーズごとに整理した、厳選30個のDevSecOpsツール。" +template = "tools" ++++ + +DevSecOpsを適用する時間の多くは、ツールを探し、比較し、選ぶことに費やされます。このページでは、厳選したツールをループのどのフェーズに属するかで整理しました。良いツールが抜けていたら、[コントリビューション歓迎です](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md)。 diff --git a/website/content/tools/_index.ko.md b/website/content/tools/_index.ko.md new file mode 100644 index 0000000..0d194bd --- /dev/null +++ b/website/content/tools/_index.ko.md @@ -0,0 +1,7 @@ ++++ +title = "도구" +description = "루프의 단계별로 정리한 30개의 엄선된 DevSecOps 도구." +template = "tools" ++++ + +DevSecOps를 적용하는 시간의 상당 부분은 도구를 찾고, 비교하고, 고르는 데 쓰여요. 이 페이지는 엄선된 도구들을 루프의 어느 단계에 사는지 기준으로 한곳에 모았습니다. 좋은 도구가 빠져 있나요? [기여를 환영합니다](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md). diff --git a/website/content/tools/_index.md b/website/content/tools/_index.md new file mode 100644 index 0000000..089cab9 --- /dev/null +++ b/website/content/tools/_index.md @@ -0,0 +1,7 @@ ++++ +title = "Tools" +description = "A curated arsenal of 30 DevSecOps tools, grouped by phase of the loop." +template = "tools" ++++ + +Most of the time spent applying DevSecOps goes into searching, comparing, and deciding between tools. This arsenal keeps the curated ones in one place, grouped by where they live on the loop. Missing a good one? [Contributions are welcome](https://github.com/hahwul/DevSecOps/blob/main/CONTRIBUTING.md). diff --git a/website/data/chapters.yml b/website/data/chapters.yml new file mode 100644 index 0000000..c388cff --- /dev/null +++ b/website/data/chapters.yml @@ -0,0 +1,20 @@ +# The DevSecOps loop, in order. slug doubles as section path, phase key, +# and icon name. caps keys resolve through i18n "caps.*". +- slug: design + episode: 1 + caps: [secure_sdlc, threat_modeling] +- slug: develop + episode: 2 + caps: [secure_coding, code_auth, repo_access] +- slug: build + episode: 3 + caps: [sast, sca, secret_mgmt, iast] +- slug: test + episode: 4 + caps: [dast, iast, pentest] +- slug: deploy + episode: 5 + caps: [hardening, config] +- slug: operate + episode: 6 + caps: [rasp, audit, monitor, patch] diff --git a/website/data/tools.yml b/website/data/tools.yml new file mode 100644 index 0000000..f2b6dd7 --- /dev/null +++ b/website/data/tools.yml @@ -0,0 +1,231 @@ +# Curated DevSecOps tools, transcribed from tools/README.md. +# phase: design | build | test | operate (only phases present in the source). +# Descriptions are normalized to about one sentence; _ko/_ja are translations. + +# Design / THREAT +- name: OWASP Threat Dragon + phase: design + category: THREAT + url: https://github.com/mike-goodwin/owasp-threat-dragon-desktop + description: "Installable desktop variant of OWASP Threat Dragon for diagram-based threat modeling." + description_ko: "다이어그램 기반 위협 모델링을 위한 OWASP Threat Dragon의 데스크톱 버전입니다." + description_ja: "図ベースの脅威モデリングを行うOWASP Threat Dragonのデスクトップ版。" +- name: pytm + phase: design + category: THREAT + url: https://github.com/izar/pytm + description: "A Pythonic framework for threat modeling: describe your system in code, get diagrams and threats." + description_ko: "파이썬 코드로 시스템을 기술하면 다이어그램과 위협 목록을 만들어 주는 위협 모델링 프레임워크입니다." + description_ja: "システムをPythonコードで記述すると、図と脅威リストを生成する脅威モデリングフレームワーク。" +- name: SeaSponge + phase: design + category: THREAT + url: https://github.com/mozilla/seasponge + description: "An accessible browser-based threat modeling tool from Mozilla." + description_ko: "Mozilla가 만든 접근성 좋은 브라우저 기반 위협 모델링 도구입니다." + description_ja: "Mozilla製のアクセシブルなブラウザベース脅威モデリングツール。" +- name: Threagile + phase: design + category: THREAT + url: https://github.com/Threagile/threagile + description: "Agile threat modeling toolkit: model your architecture in YAML and generate risk reports." + description_ko: "아키텍처를 YAML로 모델링하면 리스크 리포트를 생성해 주는 애자일 위협 모델링 툴킷입니다." + description_ja: "アーキテクチャをYAMLでモデリングしてリスクレポートを生成するアジャイル脅威モデリングツールキット。" + +# Build / SAST +- name: Gitleaks + phase: build + category: SAST + url: https://github.com/gitleaks/gitleaks + description: "Detects hardcoded secrets like API keys, tokens, and passwords in Git repositories." + description_ko: "Git 저장소에 하드코딩된 API 키, 토큰, 비밀번호 같은 시크릿을 탐지합니다." + description_ja: "GitリポジトリにハードコードされたAPIキーやトークンなどのシークレットを検出。" +- name: SonarQube + phase: build + category: SAST + url: https://www.sonarqube.org/ + description: "Continuous code inspection platform that finds bugs, code smells, and vulnerabilities in 20+ languages." + description_ko: "20개 이상 언어에서 버그, 코드 스멜, 취약점을 찾아 주는 지속적 코드 검사 플랫폼입니다." + description_ja: "20以上の言語でバグ、コードスメル、脆弱性を検出する継続的コード検査プラットフォーム。" +- name: CodeQL + phase: build + category: SAST + url: https://github.com/github/codeql + description: "GitHub's semantic code analysis engine: query your code like data to find vulnerabilities." + description_ko: "코드를 데이터처럼 쿼리해 취약점을 찾는 GitHub의 시맨틱 코드 분석 엔진입니다." + description_ja: "コードをデータのようにクエリして脆弱性を見つけるGitHubのセマンティックコード分析エンジン。" +- name: Checkov + phase: build + category: SAST + url: https://github.com/bridgecrewio/checkov + description: "Static analysis for infrastructure as code: scans Terraform, CloudFormation, Kubernetes, and more." + description_ko: "Terraform, CloudFormation, Kubernetes 등 IaC의 설정 오류를 찾는 정적 분석 도구입니다." + description_ja: "TerraformやCloudFormation、KubernetesなどIaCの設定ミスを検出する静的解析ツール。" +- name: ggshield + phase: build + category: SAST + url: https://github.com/GitGuardian/ggshield + description: "GitGuardian CLI that detects 350+ types of hardcoded secrets and 70+ IaC misconfigurations." + description_ko: "350여 종의 하드코딩된 시크릿과 70여 종의 IaC 설정 오류를 탐지하는 GitGuardian CLI입니다." + description_ja: "350種以上のシークレットと70種以上のIaC設定ミスを検出するGitGuardianのCLI。" +- name: Semgrep + phase: build + category: SAST + url: https://github.com/returntocorp/semgrep + description: "Lightweight static analysis for many languages: find bug variants with patterns that look like source code." + description_ko: "소스 코드처럼 생긴 패턴으로 버그 변종을 찾는 가벼운 다국어 정적 분석 도구입니다." + description_ja: "ソースコードに似たパターンでバグの亜種を探す軽量な多言語静的解析ツール。" +- name: sonarcloud-github-action + phase: build + category: SAST + url: https://github.com/SonarSource/sonarcloud-github-action + description: "Integrates SonarCloud code analysis into GitHub Actions workflows." + description_ko: "SonarCloud 코드 분석을 GitHub Actions 워크플로에 통합합니다." + description_ja: "SonarCloudのコード分析をGitHub Actionsのワークフローに統合。" + +# Build / SECRET-MANAGE +- name: Kamus + phase: build + category: SECRET-MANAGE + url: https://github.com/Soluto/kamus + description: "Open source, GitOps, zero-trust secret encryption and decryption for Kubernetes applications." + description_ko: "쿠버네티스 애플리케이션을 위한 오픈소스 GitOps 제로 트러스트 시크릿 암복호화 솔루션입니다." + description_ja: "Kubernetesアプリ向けのオープンソースGitOpsゼロトラストシークレット暗号化ソリューション。" +- name: secrets-sync-action + phase: build + category: SECRET-MANAGE + url: https://github.com/google/secrets-sync-action + description: "A GitHub Action that syncs secrets from one repository to many others." + description_ko: "한 저장소의 시크릿을 여러 저장소로 동기화해 주는 GitHub Action입니다." + description_ja: "1つのリポジトリのシークレットを複数のリポジトリへ同期するGitHub Action。" +- name: vault-action + phase: build + category: SECRET-MANAGE + url: https://github.com/hashicorp/vault-action + description: "A GitHub Action for using HashiCorp Vault secrets as build variables." + description_ko: "HashiCorp Vault의 시크릿을 빌드 변수로 사용하게 해 주는 GitHub Action입니다." + description_ja: "HashiCorp Vaultのシークレットをビルド変数として使うためのGitHub Action。" + +# Test / DAST +- name: zaproxy + phase: test + category: DAST + url: https://github.com/zaproxy/zaproxy + description: "The ZAP core project: the classic open source DAST scanner and intercepting proxy." + description_ko: "대표적인 오픈소스 DAST 스캐너이자 가로채기 프록시인 ZAP의 코어 프로젝트입니다." + description_ja: "定番のオープンソースDASTスキャナ兼インターセプトプロキシ、ZAPのコアプロジェクト。" +- name: action-baseline + phase: test + category: DAST + url: https://github.com/zaproxy/action-baseline + description: "A GitHub Action that runs the ZAP baseline scan against your running app." + description_ko: "실행 중인 앱에 ZAP 베이스라인 스캔을 돌려 주는 GitHub Action입니다." + description_ja: "稼働中のアプリに対してZAPベースラインスキャンを実行するGitHub Action。" +- name: action-dalfox + phase: test + category: DAST + url: https://github.com/hahwul/action-dalfox + description: "XSS scanning with Dalfox on GitHub Actions." + description_ko: "GitHub Actions에서 Dalfox로 XSS를 스캔합니다." + description_ja: "GitHub Actions上でDalfoxによるXSSスキャンを実行。" +- name: action-full-scan + phase: test + category: DAST + url: https://github.com/zaproxy/action-full-scan + description: "A GitHub Action that runs the full ZAP scan, including active attacks." + description_ko: "능동 공격을 포함한 ZAP 풀 스캔을 실행하는 GitHub Action입니다." + description_ja: "アクティブ攻撃を含むZAPフルスキャンを実行するGitHub Action。" + +# Test / PENTEST +- name: Faraday + phase: test + category: PENTEST + url: https://github.com/infobyte/faraday + description: "Collaborative penetration test and vulnerability management platform." + description_ko: "협업형 모의해킹 및 취약점 관리 플랫폼입니다." + description_ja: "共同作業型のペネトレーションテストと脆弱性管理のプラットフォーム。" +- name: Metasploit Framework + phase: test + category: PENTEST + url: https://github.com/rapid7/metasploit-framework + description: "The widely used penetration testing and exploitation framework." + description_ko: "가장 널리 쓰이는 모의해킹, 익스플로잇 프레임워크입니다." + description_ja: "最も広く使われているペネトレーションテストとエクスプロイトのフレームワーク。" +- name: Infection Monkey + phase: test + category: PENTEST + url: https://github.com/guardicore/monkey + description: "An automated pentest tool that safely simulates breach and attack scenarios." + description_ko: "침해와 공격 시나리오를 안전하게 시뮬레이션하는 자동화 모의해킹 도구입니다." + description_ja: "侵害と攻撃のシナリオを安全にシミュレーションする自動ペンテストツール。" +- name: Nuclei + phase: test + category: PENTEST + url: https://github.com/projectdiscovery/nuclei + description: "Fast and customizable vulnerability scanner based on a simple YAML DSL." + description_ko: "간단한 YAML DSL 기반의 빠르고 커스터마이즈 가능한 취약점 스캐너입니다." + description_ja: "シンプルなYAML DSLベースの高速でカスタマイズ可能な脆弱性スキャナ。" +- name: Penetrify + phase: test + category: PENTEST + url: https://www.penetrify.cloud/ + description: "Autonomous AI penetration testing platform that exploits and chains vulnerabilities, with CI/CD integration." + description_ko: "취약점을 익스플로잇하고 연계하는 자율 AI 모의해킹 플랫폼으로, CI/CD 통합을 지원합니다." + description_ja: "脆弱性のエクスプロイトと連鎖を行う自律型AIペンテストプラットフォーム。CI/CD統合に対応。" +- name: PTF + phase: test + category: PENTEST + url: https://github.com/trustedsec/ptf + description: "The Penetration Testers Framework: modular support for keeping pentest tools up to date." + description_ko: "모의해킹 도구를 모듈식으로 최신 상태로 유지해 주는 Penetration Testers Framework입니다." + description_ja: "ペンテストツールをモジュール式に最新へ保つPenetration Testers Framework。" + +# Operate / COMPONENT-ANALYSIS +- name: releaserun + phase: operate + category: COMPONENT-ANALYSIS + url: https://github.com/Releaserun/releaserun-cli + description: "Scans dependencies for end-of-life runtimes, known CVEs, and version health across 300+ technologies." + description_ko: "300개 이상의 기술에서 지원 종료 런타임, 알려진 CVE, 버전 상태를 스캔합니다." + description_ja: "300以上の技術を対象にEOLランタイム、既知のCVE、バージョン健全性をスキャン。" +- name: Dependency-Track + phase: operate + category: COMPONENT-ANALYSIS + url: https://github.com/DependencyTrack/dependency-track + description: "Intelligent component analysis platform for reducing software supply chain risk." + description_ko: "소프트웨어 공급망 위험을 식별하고 줄여 주는 지능형 컴포넌트 분석 플랫폼입니다." + description_ja: "ソフトウェアサプライチェーンのリスクを減らすインテリジェントなコンポーネント分析プラットフォーム。" + +# Operate / K8S +- name: kube-hunter + phase: operate + category: K8S + url: https://github.com/aquasecurity/kube-hunter + description: "Hunts for security weaknesses in Kubernetes clusters." + description_ko: "쿠버네티스 클러스터의 보안 약점을 사냥하듯 찾아냅니다." + description_ja: "Kubernetesクラスタのセキュリティ上の弱点をハンティング。" +- name: KubeStellar Console + phase: operate + category: K8S + url: https://github.com/kubestellar/console + description: "Multi-cluster Kubernetes dashboard with supply chain security, policy dashboards, and AI-assisted triage." + description_ko: "공급망 보안, 정책 대시보드, AI 트리아지를 갖춘 멀티 클러스터 쿠버네티스 대시보드입니다." + description_ja: "サプライチェーンセキュリティやポリシーダッシュボード、AIトリアージを備えたマルチクラスタKubernetesダッシュボード。" + +# Operate / SECURITY-AUDIT +- name: Prowler + phase: operate + category: SECURITY-AUDIT + url: https://github.com/prowler-cloud/prowler + description: "Cloud security auditing with 100+ checks for standards like CIS, GDPR, and HIPAA." + description_ko: "CIS, GDPR, HIPAA 같은 표준에 대한 100개 이상의 점검을 제공하는 클라우드 보안 감사 도구입니다." + description_ja: "CISやGDPR、HIPAAなどの標準に対する100以上のチェックを備えたクラウドセキュリティ監査ツール。" + +# Operate / SECURITY-SCAN +- name: Trivy + phase: operate + category: SECURITY-SCAN + url: https://github.com/aquasecurity/trivy + description: "All-in-one security scanner for container images, file systems, and Git repositories." + description_ko: "컨테이너 이미지, 파일 시스템, Git 저장소를 아우르는 올인원 보안 스캐너입니다." + description_ja: "コンテナイメージ、ファイルシステム、Gitリポジトリを対象とするオールインワンのセキュリティスキャナ。" diff --git a/website/i18n/en.toml b/website/i18n/en.toml new file mode 100644 index 0000000..be01b22 --- /dev/null +++ b/website/i18n/en.toml @@ -0,0 +1,94 @@ +# UI strings for the default language (English). +# Nested tables flatten to dot keys: {{ "nav.tools" | t }} + +[nav] +home = "Home" +design = "Design" +develop = "Develop" +build = "Build" +test = "Test" +deploy = "Deploy" +operate = "Operate" +tools = "Tools" +about = "About" + +[ui] +skip = "Skip to content" +theme = "Theme" +lang = "Language" +github = "GitHub repository" + +[footer] +line = "A comic field guide to DevSecOps." +license = "MIT licensed" + +[hero] +start = "Start Episode 1" +tools = "Meet the tools" + +[home] +read_episode = "Read episode" +cta_start = "Start with Design" +cta_github = "Star on GitHub" + +[chapter] +episode = "Episode" +prev = "Previous episode" +next = "Next episode" +loop_again = "The loop continues" +progress = "All episodes" +tools_heading = "Tools for this phase" +tools_empty = "No curated tools for this phase yet. Browse the full arsenal instead." +all_tools = "See all tools" + +[tools] +filter_all = "All" +open_repo = "Open repository" + +[notfound] +title = "This panel is missing" +body = "The Bug ate this page. Head back to the story." +back = "Back to home" + +[caps] +secure_sdlc = "Secure SDLC" +threat_modeling = "Threat Modeling" +secure_coding = "Secure Coding" +code_auth = "Code Authentication" +repo_access = "Repository Access Control" +sast = "SAST" +sca = "SCA" +secret_mgmt = "Secret Management" +iast = "IAST" +dast = "DAST" +pentest = "Pentest" +hardening = "Hardening" +config = "Configuration" +rasp = "RASP" +audit = "Audit" +monitor = "Monitoring" +patch = "Patching" + +[chapters.design] +blurb = "Threat modeling and a secure SDLC, before the first commit." +beat = "Find the villain while it is still on paper." + +[chapters.develop] +blurb = "Secure coding habits, signed code, controlled access." +beat = "Write it once, write it safe." + +[chapters.build] +blurb = "SAST, SCA, and secret scanning on every push." +beat = "The pipeline checks every commit, so we do not have to." + +[chapters.test] +blurb = "DAST and pentesting against the running app." +beat = "Attack it ourselves, before anyone else does." + +[chapters.deploy] +blurb = "Hardened hosts and safe configuration by default." +beat = "Lock every door before opening the shop." + +[chapters.operate] +blurb = "RASP, audits, monitoring, and patching in production." +beat = "Shipping is not the finish line. We keep watch." diff --git a/website/i18n/ja.toml b/website/i18n/ja.toml new file mode 100644 index 0000000..6fdde42 --- /dev/null +++ b/website/i18n/ja.toml @@ -0,0 +1,93 @@ +# 日本語UI文字列 + +[nav] +home = "ホーム" +design = "設計" +develop = "開発" +build = "ビルド" +test = "テスト" +deploy = "デプロイ" +operate = "運用" +tools = "ツール" +about = "概要" + +[ui] +skip = "本文へスキップ" +theme = "テーマ" +lang = "言語" +github = "GitHubリポジトリ" + +[footer] +line = "マンガで学ぶDevSecOpsフィールドガイド。" +license = "MITライセンス" + +[hero] +start = "エピソード1を読む" +tools = "ツールを見る" + +[home] +read_episode = "エピソードを読む" +cta_start = "設計から始める" +cta_github = "GitHubでスターを付ける" + +[chapter] +episode = "エピソード" +prev = "前のエピソード" +next = "次のエピソード" +loop_again = "ループは続く" +progress = "全エピソード" +tools_heading = "このフェーズのツール" +tools_empty = "このフェーズのツールはまだ登録されていません。ツール一覧をご覧ください。" +all_tools = "すべてのツールを見る" + +[tools] +filter_all = "すべて" +open_repo = "リポジトリを開く" + +[notfound] +title = "このコマは見つかりません" +body = "バグがこのページを食べてしまいました。ストーリーに戻りましょう。" +back = "ホームに戻る" + +[caps] +secure_sdlc = "セキュアSDLC" +threat_modeling = "脅威モデリング" +secure_coding = "セキュアコーディング" +code_auth = "コード認証" +repo_access = "リポジトリアクセス制御" +sast = "SAST" +sca = "SCA" +secret_mgmt = "シークレット管理" +iast = "IAST" +dast = "DAST" +pentest = "ペンテスト" +hardening = "ハードニング" +config = "設定" +rasp = "RASP" +audit = "監査" +monitor = "監視" +patch = "パッチ適用" + +[chapters.design] +blurb = "最初のコミットの前に行う脅威モデリングとセキュアSDLC。" +beat = "悪役は、まだ紙の上にいるうちに見つけよう。" + +[chapters.develop] +blurb = "セキュアコーディングの習慣、コード署名、アクセス制御。" +beat = "書くなら一度で、安全に。" + +[chapters.build] +blurb = "プッシュのたびにSAST、SCA、シークレットスキャン。" +beat = "パイプラインが全コミットをチェックしてくれる。" + +[chapters.test] +blurb = "動いているアプリへのDASTとペンテスト。" +beat = "誰かに攻撃される前に、自分たちで攻撃する。" + +[chapters.deploy] +blurb = "デフォルトからハードニングされたホストと安全な設定。" +beat = "店を開ける前に、すべてのドアに鍵をかける。" + +[chapters.operate] +blurb = "本番環境でのRASP、監査、監視、パッチ適用。" +beat = "リリースはゴールじゃない。見張りは続く。" diff --git a/website/i18n/ko.toml b/website/i18n/ko.toml new file mode 100644 index 0000000..4ae318b --- /dev/null +++ b/website/i18n/ko.toml @@ -0,0 +1,93 @@ +# 한국어 UI 문자열 + +[nav] +home = "홈" +design = "설계" +develop = "개발" +build = "빌드" +test = "테스트" +deploy = "배포" +operate = "운영" +tools = "도구" +about = "소개" + +[ui] +skip = "본문으로 건너뛰기" +theme = "테마" +lang = "언어" +github = "GitHub 저장소" + +[footer] +line = "만화로 배우는 DevSecOps 필드 가이드." +license = "MIT 라이선스" + +[hero] +start = "에피소드 1 시작" +tools = "도구 살펴보기" + +[home] +read_episode = "에피소드 읽기" +cta_start = "설계부터 시작하기" +cta_github = "GitHub에서 스타 누르기" + +[chapter] +episode = "에피소드" +prev = "이전 에피소드" +next = "다음 에피소드" +loop_again = "루프는 계속됩니다" +progress = "전체 에피소드" +tools_heading = "이 단계의 도구" +tools_empty = "이 단계에 등록된 도구가 아직 없어요. 전체 도구 목록을 둘러보세요." +all_tools = "모든 도구 보기" + +[tools] +filter_all = "전체" +open_repo = "저장소 열기" + +[notfound] +title = "이 컷은 존재하지 않아요" +body = "버그가 이 페이지를 먹어버렸어요. 이야기로 돌아갈까요?" +back = "홈으로 돌아가기" + +[caps] +secure_sdlc = "시큐어 SDLC" +threat_modeling = "위협 모델링" +secure_coding = "시큐어 코딩" +code_auth = "코드 인증" +repo_access = "저장소 접근 제어" +sast = "SAST" +sca = "SCA" +secret_mgmt = "시크릿 관리" +iast = "IAST" +dast = "DAST" +pentest = "모의해킹" +hardening = "하드닝" +config = "설정" +rasp = "RASP" +audit = "감사" +monitor = "모니터링" +patch = "패치" + +[chapters.design] +blurb = "첫 커밋 전에 시작하는 위협 모델링과 시큐어 SDLC." +beat = "빌런은 아직 종이 위에 있을 때 찾아야 해요." + +[chapters.develop] +blurb = "시큐어 코딩 습관, 코드 서명, 접근 제어." +beat = "한 번 쓸 때, 안전하게 쓰자." + +[chapters.build] +blurb = "모든 푸시마다 SAST, SCA, 시크릿 스캔." +beat = "파이프라인이 커밋마다 검사해 주니까요." + +[chapters.test] +blurb = "실행 중인 앱을 겨냥한 DAST와 모의해킹." +beat = "누군가 공격하기 전에, 우리가 먼저 공격해요." + +[chapters.deploy] +blurb = "기본값부터 하드닝된 호스트와 안전한 설정." +beat = "가게 문을 열기 전에 모든 문을 잠가요." + +[chapters.operate] +blurb = "운영 환경의 RASP, 감사, 모니터링, 패치." +beat = "배포는 결승선이 아니에요. 우리는 계속 지켜봐요." diff --git a/website/static/CNAME b/website/static/CNAME new file mode 100644 index 0000000..9bc1f4a --- /dev/null +++ b/website/static/CNAME @@ -0,0 +1 @@ +devsecops.hahwul.com diff --git a/website/static/css/site.css b/website/static/css/site.css new file mode 100644 index 0000000..d34a344 --- /dev/null +++ b/website/static/css/site.css @@ -0,0 +1,886 @@ +/* ========================================================================== + DevSecOps comic field guide - "Clean Webtoon" design system + Soft slate ink, paper surfaces, one mint accent + one apricot support, + pastel character fills. All colors are light-dark() pairs; the theme + switcher pins one side via [data-theme] (see the pinning rules below). + ========================================================================== */ + +/* -------------------------------------------------------------------------- + 1. Tokens + -------------------------------------------------------------------------- */ +:root { + color-scheme: light dark; + + /* Ink ramp: soft slate, never pure black. */ + --ink: light-dark(#39414a, #e9edf2); + --ink-soft: light-dark(#5b6570, #a9b3be); + --ink-faint: light-dark(#79848f, #77828d); + + /* Paper surfaces. */ + --paper: light-dark(#fbfaf7, #14181d); + --paper-raised: light-dark(#ffffff, #212934); + --paper-tint: light-dark(#f2f0ea, #1b222b); + --paper-code: light-dark(#f4f2ec, #10141a); + + /* Line work: panel borders, hairlines. */ + --line: light-dark(#3a424b, #8b98a6); + --line-soft: light-dark(#e2ded4, #2c343d); + + /* Accent 1: mint teal (Sec, links, primary CTA). */ + --accent: light-dark(#17756a, #6fd0c2); + --accent-strong: light-dark(#0f5a51, #97ddd3); + --accent-tint: color-mix(in srgb, var(--accent) 10%, transparent); + --on-accent: light-dark(#ffffff, #101418); + + /* Accent 2: warm apricot (Dev, highlights). Decorative and large text only. */ + --warm: light-dark(#c96a48, #f0a284); + --warm-deep: light-dark(#a84f30, #f4b49c); + --warm-tint: color-mix(in srgb, var(--warm) 12%, transparent); + + /* Character fills: pastel in light, rich mid-tones in dark so the cast + stays colorful against dark paper. Strokes stay --ink. */ + --c-dev: light-dark(#ffd8b5, #b07f53); + --c-sec: light-dark(#bfe7de, #47867a); + --c-ops: light-dark(#ccd9e5, #5f7488); + --c-bug: light-dark(#f2b8ab, #ad6355); + --c-blush: light-dark(#f6c5bc, #c47a6c); + + /* Character shading system: -sh darkens (screentone shadow), -hi lightens + (sheen). Both derive from the fill so they track light and dark. */ + --c-dev-sh: color-mix(in srgb, var(--c-dev) 76%, #20262e); + --c-dev-hi: color-mix(in srgb, var(--c-dev) 55%, #ffffff); + --c-sec-sh: color-mix(in srgb, var(--c-sec) 76%, #20262e); + --c-sec-hi: color-mix(in srgb, var(--c-sec) 55%, #ffffff); + --c-ops-sh: color-mix(in srgb, var(--c-ops) 76%, #20262e); + --c-ops-hi: color-mix(in srgb, var(--c-ops) 55%, #ffffff); + --c-bug-sh: color-mix(in srgb, var(--c-bug) 76%, #20262e); + --c-bug-hi: color-mix(in srgb, var(--c-bug) 55%, #ffffff); + --shade: rgba(24, 30, 38, 0.10); + --ground: light-dark(rgba(57, 65, 74, 0.13), rgba(0, 0, 0, 0.4)); + + /* Webtoon halftone texture (decorative backgrounds only). */ + --dots: color-mix(in srgb, var(--ink) 8%, transparent); + + --selection: color-mix(in srgb, var(--accent) 22%, transparent); + --glass: color-mix(in srgb, var(--paper) 82%, transparent); + + /* Syntax tokens (hljs classes, server-side highlighting). */ + --code-comment: light-dark(#8a95a1, #78838f); + --code-keyword: light-dark(#b0532f, #f0a284); + --code-string: light-dark(#177566, #7fd4c6); + --code-number: light-dark(#96690f, #dfb35f); + --code-func: light-dark(#33628f, #92badd); + --code-type: light-dark(#7a5a20, #d3b078); + --code-variable: light-dark(#8a4a3a, #e8b0a0); + --code-attr: light-dark(#45617a, #98b8cd); + --code-symbol: light-dark(#875672, #cf9fb9); + + /* Fluid type scale, minor third. */ + --step--1: clamp(0.83rem, 0.81rem + 0.11vw, 0.89rem); + --step-0: clamp(1rem, 0.96rem + 0.22vw, 1.125rem); + --step-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.4rem); + --step-2: clamp(1.44rem, 1.32rem + 0.61vw, 1.78rem); + --step-3: clamp(1.73rem, 1.53rem + 0.98vw, 2.28rem); + --step-4: clamp(2.07rem, 1.77rem + 1.52vw, 2.92rem); + --step-5: clamp(2.49rem, 2.05rem + 2.2vw, 3.85rem); + + /* Space, 8px rhythm. */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2.5rem; + --space-7: 4rem; + --space-8: 6rem; + + /* Shape system: pills for interactive, 14px panels, 22px bubbles. */ + --radius-sm: 8px; + --radius: 14px; + --radius-lg: 22px; + + /* Comic depth: flat offset, not blur. */ + --shadow-panel: 0 3px 0 light-dark(rgba(58, 66, 75, 0.16), rgba(0, 0, 0, 0.55)); + --shadow-pop: 0 12px 32px light-dark(rgba(58, 66, 75, 0.12), rgba(0, 0, 0, 0.45)); + + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-pop: cubic-bezier(0.34, 1.4, 0.64, 1); + --transition: 0.15s var(--ease-out); + --transition-slow: 0.5s var(--ease-out); + + --measure: 66ch; + --content-w: 46rem; + --wide-w: 68rem; + + /* Faces: system-only, CJK-safe. */ + --font-display: ui-rounded, "SF Pro Rounded", "Arial Rounded MT Bold", + "Hiragino Maru Gothic ProN", "BM Jua", -apple-system, BlinkMacSystemFont, + "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", "Meiryo", sans-serif; + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", + "Apple SD Gothic Neo", "Noto Sans KR", "Hiragino Kaku Gothic ProN", + "Noto Sans JP", "Malgun Gothic", "Meiryo", sans-serif; + --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace; +} + +/* Pre-light-dark() browsers get the static light palette. */ +@supports not (color: light-dark(#000, #fff)) { + :root { + --ink: #39414a; + --ink-soft: #5b6570; + --ink-faint: #79848f; + --paper: #fbfaf7; + --paper-raised: #ffffff; + --paper-tint: #f2f0ea; + --paper-code: #f4f2ec; + --line: #3a424b; + --line-soft: #e2ded4; + --accent: #17756a; + --accent-strong: #0f5a51; + --accent-tint: rgba(23, 117, 106, 0.1); + --on-accent: #ffffff; + --warm: #c96a48; + --warm-deep: #a84f30; + --warm-tint: rgba(201, 106, 72, 0.12); + --c-dev: #ffd8b5; + --c-sec: #bfe7de; + --c-ops: #ccd9e5; + --c-bug: #f2b8ab; + --c-blush: #f6c5bc; + --c-dev-sh: #cdad90; + --c-dev-hi: #ffe9d4; + --c-sec-sh: #9bbcb4; + --c-sec-hi: #dcf2ec; + --c-ops-sh: #a5b1bc; + --c-ops-hi: #e3ebf1; + --c-bug-sh: #c4968a; + --c-bug-hi: #f8dad3; + --shade: rgba(24, 30, 38, 0.1); + --ground: rgba(57, 65, 74, 0.13); + --dots: rgba(57, 65, 74, 0.08); + --selection: rgba(23, 117, 106, 0.22); + --glass: rgba(251, 250, 247, 0.82); + --code-comment: #8a95a1; + --code-keyword: #b0532f; + --code-string: #177566; + --code-number: #96690f; + --code-func: #33628f; + --code-type: #7a5a20; + --code-variable: #8a4a3a; + --code-attr: #45617a; + --code-symbol: #875672; + --shadow-panel: 0 3px 0 rgba(58, 66, 75, 0.16); + --shadow-pop: 0 12px 32px rgba(58, 66, 75, 0.12); + } +} + +/* Manual scheme pinning by the theme switcher. */ +:root[data-theme="light"] { color-scheme: light; } +:root[data-theme="dark"] { color-scheme: dark; } + +/* -------------------------------------------------------------------------- + 2. Base + -------------------------------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + font-family: var(--font-body); + font-size: var(--step-0); + line-height: 1.75; + color: var(--ink); + background: var(--paper); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +::selection { background: var(--selection); } +[id] { scroll-margin-top: 5.5rem; } + +:lang(ko) { word-break: keep-all; } +:lang(ja) { line-break: strict; } + +@view-transition { navigation: auto; } +@media (prefers-reduced-motion: reduce) { + @view-transition { navigation: none; } +} +@media (prefers-reduced-motion: no-preference) { + .site-main { transition: opacity var(--transition-slow), translate var(--transition-slow); } + @starting-style { + .site-main { opacity: 0; translate: 0 0.4rem; } + } +} + +/* -------------------------------------------------------------------------- + 3. Masthead + -------------------------------------------------------------------------- */ +.site-header { + position: sticky; + top: 0; + z-index: 50; + view-transition-name: site-header; + background: var(--glass); + -webkit-backdrop-filter: saturate(150%) blur(14px); + backdrop-filter: saturate(150%) blur(14px); + border-bottom: 2px solid var(--line); +} +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .site-header { background: var(--paper); } +} +.site-header-inner { + max-width: var(--wide-w); + margin: 0 auto; + padding: var(--space-3) var(--space-5); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + min-height: 3.5rem; +} +.site-logo { + display: inline-flex; + align-items: center; + gap: 0.55rem; + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step-1); + letter-spacing: -0.01em; + color: var(--ink); + text-decoration: none; +} +.site-logo svg { width: 1.7em; height: auto; flex: none; } +.site-logo:hover { color: var(--accent); } +.site-header-right { display: flex; align-items: center; gap: var(--space-4); } +.site-nav { display: flex; gap: var(--space-4); } +.site-nav a { + color: var(--ink-soft); + text-decoration: none; + font-size: var(--step--1); + font-weight: 600; + padding-bottom: 2px; + border-bottom: 2px solid transparent; + transition: color var(--transition), border-color var(--transition); +} +.site-nav a:hover { color: var(--accent); } +.site-nav a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); } + +.lang-switcher { display: flex; gap: 2px; padding: 2px; border: 1.5px solid var(--line-soft); border-radius: 999px; } +.lang-switcher a { + padding: 0.05rem 0.55rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; + text-decoration: none; + color: var(--ink-faint); + transition: color var(--transition), background var(--transition); +} +.lang-switcher a:hover { color: var(--accent); } +.lang-switcher a[aria-current="true"] { background: var(--ink); color: var(--paper); } + +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + border: 1.5px solid var(--line-soft); + border-radius: 999px; + background: transparent; + color: var(--ink-soft); + cursor: pointer; + transition: color var(--transition), border-color var(--transition), transform 0.1s var(--ease-out); +} +.theme-toggle:hover { color: var(--accent); border-color: var(--accent); } +.theme-toggle:active { transform: scale(0.92); } +.theme-toggle svg { display: none; } +.theme-toggle[data-mode="auto"] .tt-auto, +.theme-toggle[data-mode="light"] .tt-light, +.theme-toggle[data-mode="dark"] .tt-dark { display: block; } + +/* -------------------------------------------------------------------------- + 4. Page frame + footer + -------------------------------------------------------------------------- */ +.site-wrapper { max-width: var(--content-w); margin: 0 auto; padding: var(--space-7) var(--space-5) 0; } +.site-wrapper--wide { max-width: var(--wide-w); } +.site-main { min-height: calc(100dvh - 18rem); } + +.site-footer { + margin-top: var(--space-8); + border-top: 2px solid var(--line); + background: var(--paper-tint); +} +.site-footer-inner { + max-width: var(--wide-w); + margin: 0 auto; + padding: var(--space-6) var(--space-5); + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--space-3) var(--space-5); + font-size: var(--step--1); + color: var(--ink-soft); +} +.site-footer a { color: inherit; text-decoration: none; font-weight: 600; } +.site-footer a:hover { color: var(--accent); } +.footer-links { display: flex; gap: var(--space-4); } + +/* -------------------------------------------------------------------------- + 5. Typography + markdown flow + -------------------------------------------------------------------------- */ +h1, h2, h3 { + font-family: var(--font-display); + font-weight: 700; + line-height: 1.25; + color: var(--ink); + text-wrap: balance; + margin: 1.6em 0 0.5em; +} +h1 { font-size: var(--step-4); margin-top: 0; letter-spacing: -0.02em; } +h2 { font-size: var(--step-2); letter-spacing: -0.01em; } +h3 { font-size: var(--step-1); } +p { margin: 1em 0; } +p, li, figcaption { text-wrap: pretty; } +.flow p, .flow li { max-width: var(--measure); } + +a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent); text-underline-offset: 3px; transition: color var(--transition), text-decoration-color var(--transition); } +a:hover { color: var(--accent-strong); text-decoration-color: currentColor; } +.site-header a, .skip-link, .btn, .chip a, .tool-card a, .prevnext a, .rail-link, .resources a { text-decoration: none; } + +.flow ul > li::marker { color: var(--accent); } +.flow ol > li::marker { font-family: var(--font-display); font-weight: 700; color: var(--accent); } +.flow li { margin-bottom: 0.35em; } + +code { background: var(--paper-code); border: 1px solid var(--line-soft); padding: 0.1rem 0.4rem; border-radius: 6px; font-size: 0.85em; font-family: var(--font-mono); overflow-wrap: break-word; } +pre { + background: var(--paper-code); + border: 2px solid var(--line); + border-radius: var(--radius); + padding: var(--space-4) var(--space-5); + overflow-x: auto; + line-height: 1.6; + scrollbar-width: thin; + scrollbar-color: var(--line-soft) transparent; +} +pre code, pre code.hljs { background: transparent; border: none; padding: 0; font-size: 0.86em; } + +.hljs-comment, .hljs-quote { color: var(--code-comment); font-style: italic; } +.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-section, .hljs-doctag { color: var(--code-keyword); } +.hljs-string, .hljs-regexp, .hljs-addition, .hljs-meta .hljs-string { color: var(--code-string); } +.hljs-number, .hljs-built_in, .hljs-builtin-name, .hljs-bullet { color: var(--code-number); } +.hljs-title, .hljs-title.function_, .hljs-section .hljs-title { color: var(--code-func); } +.hljs-type, .hljs-class .hljs-title, .hljs-title.class_, .hljs-tag { color: var(--code-type); } +.hljs-attr, .hljs-attribute, .hljs-variable, .hljs-template-variable, .hljs-name { color: var(--code-variable); } +.hljs-selector-id, .hljs-selector-class, .hljs-selector-attr { color: var(--code-attr); } +.hljs-symbol, .hljs-link, .hljs-meta, .hljs-params { color: var(--code-symbol); } +.hljs-deletion { color: var(--code-keyword); } +.hljs-emphasis { font-style: italic; } +.hljs-strong { font-weight: 700; } + +pre code .line.hl { display: inline-block; width: 100%; background: color-mix(in srgb, var(--warm) 14%, transparent); } +pre code .ln { user-select: none; -webkit-user-select: none; opacity: 0.45; } +.code-block { position: relative; margin: var(--space-5) 0; } +.code-block > pre { margin: 0; border-top-left-radius: 0; border-top-right-radius: 0; } +.code-filename { padding: 0.35rem 0.9rem; font-family: var(--font-mono); font-size: 0.78rem; color: var(--ink-soft); background: var(--paper-tint); border: 2px solid var(--line); border-bottom: 0; border-radius: var(--radius) var(--radius) 0 0; } +.code-wrapper { position: relative; } +.code-copy-btn.code-copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.65rem; font-family: var(--font-mono); font-size: 0.72rem; color: var(--ink-soft); background: var(--paper-raised); border: 1.5px solid var(--line-soft); border-radius: 999px; opacity: 0; cursor: pointer; transition: opacity 0.15s ease; } +.code-wrapper:hover .code-copy-btn.code-copy-btn, .code-block:hover .code-copy-btn.code-copy-btn, .code-copy-btn.code-copy-btn:focus-visible { opacity: 1; } +.code-copy-btn.code-copy-btn.copied { opacity: 1; color: var(--accent); border-color: var(--accent); } + +img { max-width: 100%; height: auto; border-radius: var(--radius-sm); } + +blockquote { + margin: var(--space-5) 0; + padding: var(--space-4) var(--space-5); + border: 2px dashed var(--line); + border-radius: var(--radius-lg); + background: var(--paper-raised); + color: var(--ink-soft); + font-size: 1.02em; +} +blockquote p { margin: 0.35em 0; } + +table { border-collapse: collapse; width: 100%; margin: 1em 0; font-size: 0.95em; } +th, td { border-bottom: 1px solid var(--line-soft); padding: 0.55rem 0.75rem; text-align: left; } +th { font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ink-soft); border-bottom: 2px solid var(--line); } + +hr { border: none; height: 2px; margin: var(--space-7) auto; max-width: 8rem; border-radius: 999px; background: var(--line-soft); } + +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } + +/* -------------------------------------------------------------------------- + 6. Buttons + chips + -------------------------------------------------------------------------- */ +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1.4rem; + border-radius: 999px; + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step-0); + line-height: 1.3; + white-space: nowrap; + cursor: pointer; + transition: transform 0.1s var(--ease-out), background var(--transition), color var(--transition), border-color var(--transition); +} +.btn:active { transform: translateY(1px) scale(0.985); } +.btn--primary { background: var(--accent); color: var(--on-accent); border: 2px solid transparent; box-shadow: var(--shadow-panel); } +.btn--primary:hover { background: var(--accent-strong); color: var(--on-accent); } +.btn--ghost { background: var(--paper-raised); color: var(--ink); border: 2px solid var(--line); box-shadow: var(--shadow-panel); } +.btn--ghost:hover { color: var(--accent); border-color: var(--accent); } + +.chip { + display: inline-flex; + align-items: center; + padding: 0.12rem 0.7rem; + border: 1.5px solid var(--line); + border-radius: 999px; + background: var(--paper-raised); + font-size: var(--step--1); + font-weight: 600; + color: var(--ink-soft); + white-space: nowrap; +} + +/* Capability chip grid (caps shortcode renders a ul). */ +.caps ul { list-style: none; display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0; margin: var(--space-4) 0; } +.caps li { + display: inline-flex; + align-items: center; + padding: 0.18rem 0.8rem; + border: 1.5px solid var(--line); + border-radius: 999px; + background: var(--accent-tint); + font-size: var(--step--1); + font-weight: 600; + color: var(--ink); + margin: 0; +} + +/* -------------------------------------------------------------------------- + 7. Comic primitives: strips, panels, bubbles, scenes + -------------------------------------------------------------------------- */ +.strip { display: flex; flex-direction: column; gap: var(--space-5); margin: var(--space-6) 0; } +@media (min-width: 760px) { + .strip--row { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); align-items: stretch; } +} + +.panel { + background: var(--paper-raised); + border: 2px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-panel); + padding: var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-4); +} +.panel > :first-child { margin-top: 0; } +.panel > :last-child { margin-bottom: 0; } +.panel--tint { background: color-mix(in srgb, var(--c-sec) 22%, var(--paper-raised)); } +.panel--tint-warm { background: color-mix(in srgb, var(--c-dev) 22%, var(--paper-raised)); } +.panel--halftone { + background-image: radial-gradient(var(--dots) 1.2px, transparent 1.2px); + background-size: 13px 13px; +} +.panel--center { align-items: center; text-align: center; } + +.panel-cap { font-size: var(--step--1); color: var(--ink-soft); margin: 0; } + +/* Character scenes always render on light paper, in both themes: the scene + pins color-scheme to light, so every light-dark() token inside (ink + strokes, pastel fills, paper) resolves to its light value. The svg gets a + white plate so panels read as printed comic cels on dark paper. */ +.scene { margin: 0; display: flex; justify-content: center; color-scheme: light; } +.scene svg { + width: 100%; + max-width: 15rem; + height: auto; + background: var(--paper-raised); + border-radius: var(--radius); + padding: var(--space-2); +} +.scene--lg svg { max-width: 24rem; } +.scene--sm svg { max-width: 9rem; } +.scene--wide svg { max-width: 38rem; } + +/* Speech bubbles: real HTML text, translatable. Tail via rotated square. */ +.bubble { + position: relative; + align-self: flex-start; + max-width: 32ch; + margin: 0 0 12px; + padding: 0.7rem 1.1rem; + background: var(--paper-raised); + border: 2px solid var(--line); + border-radius: var(--radius-lg); + font-family: var(--font-display); + font-weight: 600; + font-size: var(--step-0); + line-height: 1.5; +} +.bubble p { margin: 0; } +.bubble::after { + content: ""; + position: absolute; + bottom: -8.5px; + left: 26px; + width: 13px; + height: 13px; + background: inherit; + border-right: 2px solid var(--line); + border-bottom: 2px solid var(--line); + border-bottom-right-radius: 3px; + transform: rotate(45deg); +} +.bubble--right { align-self: flex-end; } +.bubble--right::after { left: auto; right: 26px; } +.bubble--dev { background: color-mix(in srgb, var(--c-dev) 34%, var(--paper-raised)); } +.bubble--sec { background: color-mix(in srgb, var(--c-sec) 40%, var(--paper-raised)); } +.bubble--ops { background: color-mix(in srgb, var(--c-ops) 40%, var(--paper-raised)); } +.bubble--bug { background: color-mix(in srgb, var(--c-bug) 34%, var(--paper-raised)); } +.bubble--thought { border-style: dashed; font-style: italic; } +.bubble--thought::after { display: none; } +.bubble--shout { border-width: 3px; font-weight: 700; } + +/* Episode number badge: genuine comic chapter marker. */ +.ep-mark { + display: inline-flex; + align-items: center; + gap: 0.5rem; + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step--1); + color: var(--ink); +} +.ep-mark .ep-num { + display: inline-grid; + place-items: center; + width: 2rem; + height: 2rem; + border: 2px solid var(--line); + border-radius: 999px; + background: var(--warm-tint); + color: var(--warm-deep); + font-size: 1rem; +} + +/* Resource link cards (resources shortcode wraps a markdown list). */ +.resources ul { list-style: none; padding: 0; margin: var(--space-4) 0; display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); } +.resources li { margin: 0; max-width: none; } +.resources li a { + display: block; + height: 100%; + padding: var(--space-3) var(--space-4); + background: var(--paper-raised); + border: 1.5px solid var(--line); + border-radius: var(--radius); + color: var(--ink); + font-weight: 600; + font-size: var(--step--1); + line-height: 1.5; + transition: border-color var(--transition), color var(--transition), translate 0.1s var(--ease-out); +} +.resources li a:hover { border-color: var(--accent); color: var(--accent); translate: 0 -2px; } + +/* Alert shortcode. */ +.alert { + padding: var(--space-4) var(--space-5); + border: 2px solid var(--line); + border-radius: var(--radius); + background: var(--accent-tint); + margin: var(--space-5) 0; +} +.alert--warning, .alert--danger { background: var(--warm-tint); } +.alert strong:first-child { color: var(--accent-strong); } +.alert--warning strong:first-child, .alert--danger strong:first-child { color: var(--warm-deep); } +.alert p { margin: 0.3em 0 0; } + +/* -------------------------------------------------------------------------- + 8. Home + -------------------------------------------------------------------------- */ +.hero { min-height: calc(100dvh - 3.75rem); display: flex; align-items: center; padding: var(--space-6) var(--space-5); } +.hero-inner { + max-width: var(--wide-w); + margin: 0 auto; + display: grid; + gap: var(--space-6); + align-items: center; + width: 100%; +} +@media (min-width: 900px) { + .hero-inner { grid-template-columns: 1.05fr 0.95fr; } +} +.hero-title { font-size: var(--step-5); line-height: 1.08; letter-spacing: -0.025em; margin: 0 0 var(--space-4); } +.hero-title em { font-style: normal; color: var(--accent); } +.hero-sub { font-size: var(--step-1); color: var(--ink-soft); margin: 0 0 var(--space-5); max-width: 34ch; } +.hero-ctas { display: flex; flex-wrap: wrap; gap: var(--space-3); } +.hero-art { position: relative; } +.hero-art .panel { padding: var(--space-6); } +.hero-art svg { width: 100%; height: auto; } + +/* Home markdown flow: sections are h2 + shortcode blocks. */ +.home-flow { max-width: var(--wide-w); margin: 0 auto; padding: 0 var(--space-5); } +.home-flow > h2 { margin-top: var(--space-8); font-size: var(--step-3); } +.home-flow > p { max-width: var(--measure); color: var(--ink-soft); } + +/* Loop diagram: SVG path + absolutely positioned HTML node links. */ +.loop-wrap { position: relative; margin: var(--space-6) 0; } +.loop-wrap svg { width: 100%; height: auto; display: block; } +.loop-node { + position: absolute; + transform: translate(-50%, -50%); + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.28rem 0.9rem 0.28rem 0.35rem; + background: var(--paper-raised); + border: 2px solid var(--line); + border-radius: 999px; + box-shadow: var(--shadow-panel); + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step--1); + color: var(--ink); + white-space: nowrap; + transition: border-color var(--transition), color var(--transition); +} +.loop-node:hover { border-color: var(--accent); color: var(--accent); } +.loop-node .n { + display: grid; + place-items: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 999px; + border: 1.5px solid var(--line); + background: var(--warm-tint); + color: var(--warm-deep); + font-size: 0.76rem; +} +.loop-caption { text-align: center; color: var(--ink-soft); font-size: var(--step--1); margin-top: var(--space-3); } +@media (max-width: 759px) { + .loop-wrap { display: flex; flex-wrap: wrap; gap: var(--space-2); justify-content: center; } + .loop-wrap svg { flex-basis: 100%; margin-bottom: var(--space-3); } + .loop-node { position: static; transform: none; } +} + +/* Stagger siblings inside strips and rows. */ +.strip > .panel:nth-child(2) { --i: 1; } +.strip > .panel:nth-child(3) { --i: 2; } +.strip > .panel:nth-child(4) { --i: 3; } + +/* Team section: asymmetric 2+1 (wide member sheet + offset interloper card). */ +.team { display: grid; gap: var(--space-5); margin: var(--space-6) 0; align-items: start; } +@media (min-width: 900px) { + .team { grid-template-columns: 1.6fr 1fr; } + .team > :nth-child(2) { margin-top: var(--space-7); } +} +.team-member { display: grid; grid-template-columns: auto 1fr; gap: var(--space-4); align-items: center; } +.team-member .scene svg { max-width: 7.5rem; } +.team-member h3 { margin: 0 0 0.2em; } +.team-member p { margin: 0; color: var(--ink-soft); font-size: var(--step--1); } + +/* Episode rail. */ +.rail { display: flex; flex-direction: column; gap: var(--space-5); margin: var(--space-6) 0; } +.rail-item { + display: grid; + gap: var(--space-4); + grid-template-columns: 1fr; + align-items: center; +} +@media (min-width: 760px) { + .rail-item { grid-template-columns: auto 1fr auto; } + .rail-item:nth-child(even) { margin-left: var(--space-6); } + .rail-item:nth-child(odd) { margin-right: var(--space-6); } +} +.rail-icon svg { width: 4.5rem; height: 4.5rem; } +.rail-body h3 { margin: 0.3em 0 0.15em; font-size: var(--step-1); } +.rail-body .rail-blurb { margin: 0 0 var(--space-2); color: var(--ink-soft); font-size: var(--step--1); } +.rail-beat { font-family: var(--font-display); font-weight: 600; font-size: var(--step--1); color: var(--ink); margin: 0 0 var(--space-2); } +.rail-beat::before { content: "\201C"; color: var(--warm); } +.rail-beat::after { content: "\201D"; color: var(--warm); } +.rail-caps { display: flex; flex-wrap: wrap; gap: var(--space-1); padding: 0; margin: 0; list-style: none; } +.rail-caps li { display: inline-flex; padding: 0.05rem 0.6rem; border: 1.5px solid var(--line-soft); border-radius: 999px; font-size: 0.72rem; font-weight: 600; color: var(--ink-soft); } +.rail-link { + justify-self: start; + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step--1); + color: var(--accent); + white-space: nowrap; +} +.rail-link:hover { color: var(--accent-strong); } +@media (min-width: 760px) { .rail-link { justify-self: end; } } + +/* Finale banner. */ +.finale { margin: var(--space-8) 0; } +.finale .panel { align-items: center; text-align: center; padding: var(--space-7) var(--space-5); } +.finale h2 { margin: var(--space-4) 0 var(--space-2); font-size: var(--step-3); } +.finale .hero-ctas { justify-content: center; margin-top: var(--space-4); } + +/* -------------------------------------------------------------------------- + 9. Chapter pages + -------------------------------------------------------------------------- */ +.chapter-head { display: grid; gap: var(--space-4); margin-bottom: var(--space-6); } +.chapter-head-top { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; } +.chapter-title { display: flex; align-items: center; gap: var(--space-4); } +.chapter-icon svg { width: 4rem; height: 4rem; } +.chapter-head h1 { margin: 0; } +.chapter-hook { font-size: var(--step-1); color: var(--ink-soft); margin: 0; font-family: var(--font-display); } + +.chapter-progress { display: flex; gap: var(--space-2); align-items: center; padding: 0; margin: 0; list-style: none; } +.chapter-progress a { + display: block; + width: 0.7rem; + height: 0.7rem; + border-radius: 999px; + border: 2px solid var(--line); + background: var(--paper-raised); + transition: background var(--transition), transform 0.1s var(--ease-out); +} +.chapter-progress a:hover { transform: scale(1.25); } +.chapter-progress a[aria-current="page"] { background: var(--accent); border-color: var(--accent); } + +.chapter-body h2 { padding-bottom: 0.3rem; border-bottom: 2px solid var(--line-soft); } + +/* Tools grid (chapter + tools page). */ +.tools-grid { display: grid; gap: var(--space-4); grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); margin: var(--space-5) 0; padding: 0; list-style: none; } +.tool-card { + margin: 0; + max-width: none; + display: flex; + flex-direction: column; + gap: var(--space-2); + padding: var(--space-4); + background: var(--paper-raised); + border: 2px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-panel); +} +.tool-card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); } +.tool-card-head a { font-family: var(--font-display); font-weight: 700; font-size: var(--step-0); color: var(--ink); } +.tool-card-head a:hover { color: var(--accent); } +.tool-cat { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; padding: 0.1rem 0.55rem; border-radius: 999px; background: var(--accent-tint); color: var(--accent-strong); white-space: nowrap; } +.tool-card p { margin: 0; font-size: var(--step--1); color: var(--ink-soft); line-height: 1.55; } + +.tools-empty { padding: var(--space-5); border: 2px dashed var(--line); border-radius: var(--radius); color: var(--ink-soft); } +.tools-empty p { margin: 0 0 var(--space-2); } + +/* Prev/next loop nav. */ +.prevnext { display: grid; gap: var(--space-4); margin: var(--space-7) 0 0; } +@media (min-width: 640px) { .prevnext { grid-template-columns: 1fr 1fr; } } +.prevnext a { + display: flex; + flex-direction: column; + gap: 0.15rem; + padding: var(--space-4) var(--space-5); + background: var(--paper-raised); + border: 2px solid var(--line); + border-radius: var(--radius); + box-shadow: var(--shadow-panel); + color: var(--ink); + transition: border-color var(--transition), translate 0.1s var(--ease-out); +} +.prevnext a:hover { border-color: var(--accent); translate: 0 -2px; } +.prevnext .pn-label { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--ink-faint); } +.prevnext .pn-title { font-family: var(--font-display); font-weight: 700; font-size: var(--step-1); } +.prevnext .pn-next { text-align: right; align-items: flex-end; } +@media (min-width: 640px) { .prevnext .pn-prev:only-child, .prevnext .pn-next:only-child { grid-column: auto; } } + +/* -------------------------------------------------------------------------- + 10. Tools page + -------------------------------------------------------------------------- */ +.filter-row { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-5) 0; } +html:not(.js) .filter-row { display: none; } +.filter-chip { + padding: 0.25rem 0.9rem; + border: 2px solid var(--line); + border-radius: 999px; + background: var(--paper-raised); + font-family: var(--font-display); + font-weight: 700; + font-size: var(--step--1); + color: var(--ink-soft); + cursor: pointer; + transition: background var(--transition), color var(--transition), border-color var(--transition); +} +.filter-chip:hover { color: var(--accent); border-color: var(--accent); } +.filter-chip[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--paper); } +.filter-chip .count { font-weight: 600; opacity: 0.65; margin-left: 0.3rem; } + +.tools-section-head { display: flex; align-items: center; gap: var(--space-3); margin-top: var(--space-6); } +.tools-section-head svg { width: 2.4rem; height: 2.4rem; flex: none; } +.tools-section-head h2 { margin: 0; border: none; } + +[data-filter="design"] .tools-section:not([data-phase="design"]), +[data-filter="build"] .tools-section:not([data-phase="build"]), +[data-filter="test"] .tools-section:not([data-phase="test"]), +[data-filter="operate"] .tools-section:not([data-phase="operate"]) { display: none; } + +/* -------------------------------------------------------------------------- + 11. 404 + about + -------------------------------------------------------------------------- */ +.notfound { max-width: 28rem; margin: var(--space-7) auto; text-align: center; } +.notfound .scene svg { max-width: 10rem; } +.notfound h1 { font-size: var(--step-3); } + +/* -------------------------------------------------------------------------- + 12. Motion: reveal on scroll (gated on .js + motion preference) + -------------------------------------------------------------------------- */ +@media (prefers-reduced-motion: no-preference) { + html.js .reveal { + opacity: 0; + translate: 0 16px; + scale: 0.985; + transition: opacity 0.55s var(--ease-out), translate 0.55s var(--ease-out), scale 0.55s var(--ease-out); + transition-delay: calc(var(--i, 0) * 90ms); + } + html.js .reveal.is-in { opacity: 1; translate: 0 0; scale: 1; } + + html.js .reveal .bubble { + opacity: 0; + scale: 0.9; + transform-origin: 20% 100%; + transition: opacity 0.4s var(--ease-pop), scale 0.4s var(--ease-pop); + transition-delay: calc(var(--i, 0) * 90ms + 0.25s); + } + html.js .reveal.is-in .bubble { opacity: 1; scale: 1; } + + html.js .loop-draw { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 1.7s var(--ease-out) 0.2s; } + html.js .is-in .loop-draw, html:not(.js) .loop-draw { stroke-dashoffset: 0; } + + .bob { animation: bob 6s ease-in-out infinite alternate; } + @keyframes bob { + from { transform: translateY(0); } + to { transform: translateY(-7px); } + } +} + +/* -------------------------------------------------------------------------- + 13. Responsive frame + -------------------------------------------------------------------------- */ +@media (max-width: 640px) { + .site-header-inner { padding: var(--space-2) var(--space-4); flex-wrap: wrap; row-gap: var(--space-1); } + .site-wrapper { padding: var(--space-6) var(--space-4) 0; } + .hero { padding: var(--space-5) var(--space-4); min-height: auto; } + .home-flow { padding: 0 var(--space-4); } + .rail-item:nth-child(even), .rail-item:nth-child(odd) { margin-left: 0; margin-right: 0; } + .team-member { grid-template-columns: 1fr; justify-items: center; text-align: center; } +} + +/* -------------------------------------------------------------------------- + 14. Accessibility + -------------------------------------------------------------------------- */ +:focus-visible { outline: 2.5px solid var(--accent); outline-offset: 3px; border-radius: 2px; } +.skip-link { position: absolute; top: -100px; left: 0; background: var(--accent); color: var(--on-accent); padding: var(--space-2) var(--space-4); z-index: 1000; border-radius: 0 0 var(--radius-sm) 0; } +.skip-link:focus { top: 0; } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} diff --git a/website/static/favicon.svg b/website/static/favicon.svg new file mode 100644 index 0000000..0596d9a --- /dev/null +++ b/website/static/favicon.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/website/static/images/og.png b/website/static/images/og.png new file mode 100644 index 0000000..1477175 Binary files /dev/null and b/website/static/images/og.png differ diff --git a/website/static/js/site.js b/website/static/js/site.js new file mode 100644 index 0000000..cee01a8 --- /dev/null +++ b/website/static/js/site.js @@ -0,0 +1,35 @@ +(function () { + "use strict"; + + // Reveal panels as they enter the viewport. Hidden states only exist + // under html.js + prefers-reduced-motion: no-preference (see site.css), + // so no-JS and reduced-motion readers always see everything. + var els = document.querySelectorAll(".reveal"); + if ("IntersectionObserver" in window && els.length) { + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add("is-in"); + io.unobserve(entry.target); + } + }); + }, { threshold: 0.1, rootMargin: "0px 0px -8% 0px" }); + els.forEach(function (el) { io.observe(el); }); + } else { + els.forEach(function (el) { el.classList.add("is-in"); }); + } + + // Tools page phase filter: chips set data-filter on the list container, + // CSS does the showing and hiding. + var grid = document.querySelector("[data-filter]"); + if (grid) { + var chips = document.querySelectorAll(".filter-chip"); + chips.forEach(function (chip) { + chip.addEventListener("click", function () { + chips.forEach(function (c) { c.setAttribute("aria-pressed", "false"); }); + chip.setAttribute("aria-pressed", "true"); + grid.setAttribute("data-filter", chip.getAttribute("data-phase")); + }); + }); + } +})(); diff --git a/website/templates/404.html b/website/templates/404.html new file mode 100644 index 0000000..dc2ef35 --- /dev/null +++ b/website/templates/404.html @@ -0,0 +1,16 @@ +{% include "header.html" %} +
+
+
+
+ +
+

{{ "notfound.title" | t }}

+

{{ "notfound.body" | t }}

+

{{ "notfound.back" | t }}

+
+
+
+{% include "footer.html" %} diff --git a/website/templates/chapter.html b/website/templates/chapter.html new file mode 100644 index 0000000..a2a2e71 --- /dev/null +++ b/website/templates/chapter.html @@ -0,0 +1,51 @@ +{% include "header.html" %} +
+
+
+
+ {{ "chapter.episode" | t }}{{ page.extra.episode }} + +
+
+ {% set icon = page.extra.phase %}{% include "partials/svg/icon.html" %} +

{{ page.title | e }}

+
+

{{ page.extra.hook | e }}

+
+
+ {{ content }} +
+
+

{{ "chapter.tools_heading" | t }}

+ {% if page.extra.has_tools %} +
    + {% set tools = load_data(path="data/tools.yml") %} + {% for tool in tools %}{% if tool.phase == page.extra.phase %} +
  • +
    + {{ tool.name | e }} + {{ tool.category | e }} +
    +

    {% if page.language == "ko" and tool.description_ko %}{{ tool.description_ko | e }}{% elif page.language == "ja" and tool.description_ja %}{{ tool.description_ja | e }}{% else %}{{ tool.description | e }}{% endif %}

    +
  • + {% endif %}{% endfor %} +
+

{{ "chapter.all_tools" | t }} →

+ {% else %} +
+

{{ "chapter.tools_empty" | t }}

+

{{ "chapter.all_tools" | t }} →

+
+ {% endif %} +
+ {% include "partials/prevnext.html" %} +
+
+{% include "footer.html" %} diff --git a/website/templates/footer.html b/website/templates/footer.html new file mode 100644 index 0000000..43b1bed --- /dev/null +++ b/website/templates/footer.html @@ -0,0 +1,42 @@ + + {{ highlight_js }} + + + {{ auto_includes_js }} + + diff --git a/website/templates/header.html b/website/templates/header.html new file mode 100644 index 0000000..a4b854e --- /dev/null +++ b/website/templates/header.html @@ -0,0 +1,40 @@ + + + + + + + {% if page.title is present %}{{ page.title | e }} - {% endif %}{{ site.title | e }} + + + {{ og_all_tags }} + {{ canonical_tag }} + {{ jsonld }} + {{ hreflang_tags }} + {{ pagination_seo_links }} + + + {{ math_tags }} + {{ mermaid_tags }} + {{ auto_includes_css }} + + + + diff --git a/website/templates/home.html b/website/templates/home.html new file mode 100644 index 0000000..c7d73cc --- /dev/null +++ b/website/templates/home.html @@ -0,0 +1,26 @@ +{% include "header.html" %} +
+
+
+
+

{{ page.extra.hero_title | safe }}

+

{{ page.extra.hero_sub | e }}

+ +
+
+
+
+ {% include "partials/svg/trio.html" %} +
+
+
+
+
+
+ {{ content }} +
+
+{% include "footer.html" %} diff --git a/website/templates/page.html b/website/templates/page.html new file mode 100644 index 0000000..94cc2de --- /dev/null +++ b/website/templates/page.html @@ -0,0 +1,8 @@ +{% include "header.html" %} +
+
+ {% if page.title is present %}

{{ page.title | e }}

{% endif %} + {{ content }} +
+
+{% include "footer.html" %} diff --git a/website/templates/partials/lang-switcher.html b/website/templates/partials/lang-switcher.html new file mode 100644 index 0000000..f326aa3 --- /dev/null +++ b/website/templates/partials/lang-switcher.html @@ -0,0 +1,9 @@ +{% if page.translations %} + +{% endif %} diff --git a/website/templates/partials/prevnext.html b/website/templates/partials/prevnext.html new file mode 100644 index 0000000..8e6b7cc --- /dev/null +++ b/website/templates/partials/prevnext.html @@ -0,0 +1,22 @@ +{# Loop navigation between chapters. Wraps around: after Episode 6 the loop + continues at Episode 1. Position comes from page.extra.episode. #} +{% set chapters = load_data(path="data/chapters.yml") %} +{% set cur = page.extra.episode %} + diff --git a/website/templates/partials/svg/char-bug.html b/website/templates/partials/svg/char-bug.html new file mode 100644 index 0000000..e6c9cc6 --- /dev/null +++ b/website/templates/partials/svg/char-bug.html @@ -0,0 +1,60 @@ +{# The Bug: small clay ladybeetle villain with curly antennae, jointed legs, + spotted wing cases. Coordinate space 240x240. Poses: sneak | caught | flag. + Rules: strokes var(--ink), round caps; token fills + shading tokens; no . #} +{% set p = pose | default(value="sneak") %} + + {# Ground shadow #} + + {# Antennae #} + + + + {# Jointed legs, three per side #} + + + {# Body #} + + {# Wing case split + shading + sheen #} + + + + {# Spots #} + + + + + + {# Head #} + + + {# Face #} + {% if p == "caught" %} + + + + + + + {% elif p == "flag" %} + + + + + + {% else %} + + + + + + + + {% endif %} + {# White flag #} + {% if p == "flag" %} + + + + + {% endif %} + diff --git a/website/templates/partials/svg/char-dev.html b/website/templates/partials/svg/char-dev.html new file mode 100644 index 0000000..0703d7f --- /dev/null +++ b/website/templates/partials/svg/char-dev.html @@ -0,0 +1,103 @@ +{# Dev: apricot chibi with fluffy hair, round glasses, white hoodie, sneakers. + Coordinate space 240x240 (wrapper supplies the ). Poses: calm | wave | typing | worried. + Rules: strokes var(--ink), round caps; fills from --c-* / --paper tokens plus + the shared shading tokens (--c-*-sh/-hi, --shade, --ground); no . #} +{% set p = pose | default(value="calm") %} + + {# Ground shadow #} + + {# Back arms (behind body) #} + {% if p == "wave" %} + + + {% elif p == "typing" %} + + {% elif p == "worried" %} + + {% else %} + + {% endif %} + {# Legs + sneakers #} + + + + {# Hoodie body #} + + {# Cloth shading, right side #} + + {# Kangaroo pocket #} + + {# Hood collar + drawstrings #} + + + + + {# Ears #} + + + {# Head #} + + {# Head shading along the jaw #} + + {# Hair cap with a tidy scalloped fringe #} + + {# Hair sheen #} + + {# Brows #} + {% if p == "worried" %} + + {% else %} + + {% endif %} + {# Glasses #} + + + + + {# Eyes #} + {% if p == "worried" %} + + + + + {% else %} + + + + + {% endif %} + {# Mouth #} + {% if p == "worried" %} + + {% elif p == "typing" %} + + {% else %} + + + {% endif %} + {# Blush #} + {% if p != "worried" %} + + + {% endif %} + {# Front hands + props per pose #} + {% if p == "wave" %} + + + + + {% elif p == "typing" %} + + + + + + {% elif p == "worried" %} + + + + {% else %} + + + {% endif %} + diff --git a/website/templates/partials/svg/char-ops.html b/website/templates/partials/svg/char-ops.html new file mode 100644 index 0000000..cfbc562 --- /dev/null +++ b/website/templates/partials/svg/char-ops.html @@ -0,0 +1,68 @@ +{# Ops: blue-gray round guardian with a headset and a heartbeat screen, + steady and watchful. Coordinate space 240x240. Poses: calm | gear | monitor | thumbsup. + Rules: strokes var(--ink), round caps; token fills + shading tokens; no . #} +{% set p = pose | default(value="calm") %} + + {# Ground shadow #} + + {# Back arms #} + {% if p == "thumbsup" %} + + + {% elif p == "gear" %} + + + {% elif p == "monitor" %} + + {% else %} + + {% endif %} + {# Body #} + + {# Bottom-right shading #} + + {# Top-left sheen #} + + {# Headset #} + + + + + + + {# Face #} + + + + + + + + {# Heartbeat screen #} + + + {# Front hands + props #} + {% if p == "gear" %} + + + + + + + {% elif p == "monitor" %} + + + + + + + {% elif p == "thumbsup" %} + + + + + {% else %} + + + {% endif %} + diff --git a/website/templates/partials/svg/char-sec.html b/website/templates/partials/svg/char-sec.html new file mode 100644 index 0000000..5983566 --- /dev/null +++ b/website/templates/partials/svg/char-sec.html @@ -0,0 +1,55 @@ +{# Sec: mint shield-bodied guardian, calm and sharp-eyed, double-rim shield + with a verified badge. Coordinate space 240x240. Poses: calm | point | happy. + Rules: strokes var(--ink), round caps; token fills + shading tokens; no . #} +{% set p = pose | default(value="calm") %} + + {# Ground shadow #} + + {# Back arms #} + {% if p == "point" %} + + {% elif p == "happy" %} + + {% else %} + + {% endif %} + {# Shield body #} + + {# Right-side shading #} + + {# Top-left sheen #} + + {# Inner rim #} + + {# Face #} + {% if p == "happy" %} + + {% else %} + + + + + + {% endif %} + + + + {# Verified badge #} + + + {# Front hands + props #} + {% if p == "point" %} + + + + + + {% elif p == "happy" %} + + + + {% else %} + + + {% endif %} + diff --git a/website/templates/partials/svg/char.html b/website/templates/partials/svg/char.html new file mode 100644 index 0000000..c9d30be --- /dev/null +++ b/website/templates/partials/svg/char.html @@ -0,0 +1,7 @@ +{# Character dispatch: set `char` (and optionally `pose`) before including. + Emits the character ; the caller supplies the wrapper. #} +{% if char == "char-sec" %}{% include "partials/svg/char-sec.html" %} +{% elif char == "char-ops" %}{% include "partials/svg/char-ops.html" %} +{% elif char == "char-bug" %}{% include "partials/svg/char-bug.html" %} +{% else %}{% include "partials/svg/char-dev.html" %} +{% endif %} diff --git a/website/templates/partials/svg/icon.html b/website/templates/partials/svg/icon.html new file mode 100644 index 0000000..8ee166c --- /dev/null +++ b/website/templates/partials/svg/icon.html @@ -0,0 +1,30 @@ +{# Phase icon dispatch. Set `icon` to one of: + design | develop | build | test | deploy | operate + Emits a standalone 32x32 svg. Strokes var(--ink) width 3; token fills only. #} + diff --git a/website/templates/partials/svg/logo-mark.html b/website/templates/partials/svg/logo-mark.html new file mode 100644 index 0000000..3247218 --- /dev/null +++ b/website/templates/partials/svg/logo-mark.html @@ -0,0 +1,6 @@ +{# Logo mark: the DevSecOps infinity loop with a shield at the crossing. + Strokes: var(--ink). Shield fill: var(--c-sec). No text elements. #} + diff --git a/website/templates/partials/svg/loop.html b/website/templates/partials/svg/loop.html new file mode 100644 index 0000000..6bea5f2 --- /dev/null +++ b/website/templates/partials/svg/loop.html @@ -0,0 +1,17 @@ +{# The DevSecOps infinity loop. The path is SVG (draws itself on reveal); + the six phase nodes are HTML links so labels stay translatable and focusable. + Wrapped by the loop_diagram shortcode, which passes the caption. #} + +{% if caption %}

{{ caption | e }}

{% endif %} diff --git a/website/templates/partials/svg/trio.html b/website/templates/partials/svg/trio.html new file mode 100644 index 0000000..9ee92cd --- /dev/null +++ b/website/templates/partials/svg/trio.html @@ -0,0 +1,15 @@ +{# Hero group shot: Dev waves, Sec stands center, Ops gives a thumbs up. + Standalone ; composes the character partials via include. + The .bob animation lives on an inner so its CSS transform never + overrides the positioning attribute transform of the outer . #} + diff --git a/website/templates/section.html b/website/templates/section.html new file mode 100644 index 0000000..1cfb821 --- /dev/null +++ b/website/templates/section.html @@ -0,0 +1,12 @@ +{% include "header.html" %} +
+
+ {% if page.title is present %}

{{ page.title | e }}

{% endif %} + {{ content }} +
    + {{ section.list }} +
+ {{ pagination }} +
+
+{% include "footer.html" %} diff --git a/website/templates/shortcodes/alert.html b/website/templates/shortcodes/alert.html new file mode 100644 index 0000000..ca0d0cb --- /dev/null +++ b/website/templates/shortcodes/alert.html @@ -0,0 +1,5 @@ +{# Alert box. Block shortcode. Args: type="info"|"warning"|"danger"|"tip", title #} +
+ {% if title %}{{ title | e }}{% else %}{{ type | default(value="info") | upper | e }}{% endif %} + {{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/bubble.html b/website/templates/shortcodes/bubble.html new file mode 100644 index 0000000..84f453b --- /dev/null +++ b/website/templates/shortcodes/bubble.html @@ -0,0 +1,6 @@ +{# Speech bubble: real HTML text, translatable per language file. + Block shortcode. Args: who="dev"|"sec"|"ops"|"bug", dir="right", + kind="thought"|"shout", name="Speaker" (screen-reader attribution) #} +
+{% if name %}{{ name | e }}: {% endif %}{{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/caps.html b/website/templates/shortcodes/caps.html new file mode 100644 index 0000000..07eb94a --- /dev/null +++ b/website/templates/shortcodes/caps.html @@ -0,0 +1,4 @@ +{# Capability chip grid. Block shortcode wrapping a markdown list. #} +
+{{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/episode_rail.html b/website/templates/shortcodes/episode_rail.html new file mode 100644 index 0000000..896cc93 --- /dev/null +++ b/website/templates/shortcodes/episode_rail.html @@ -0,0 +1,20 @@ +{# The six episode teaser panels, driven by data/chapters.yml + i18n. + Inline shortcode, no args. #} +{% set chapters = load_data(path="data/chapters.yml") %} +
+ {% for c in chapters %} +
+
{% set icon = c.slug %}{% include "partials/svg/icon.html" %}
+
+ {{ "chapter.episode" | t }}{{ c.episode }} +

{{ ("nav." ~ c.slug) | t }}

+

{{ ("chapters." ~ c.slug ~ ".blurb") | t }}

+

{{ ("chapters." ~ c.slug ~ ".beat") | t }}

+
    + {% for cap in c.caps %}
  • {{ ("caps." ~ cap) | t }}
  • {% endfor %} +
+
+ {{ "home.read_episode" | t }} → +
+ {% endfor %} +
diff --git a/website/templates/shortcodes/finale.html b/website/templates/shortcodes/finale.html new file mode 100644 index 0000000..3e12a7a --- /dev/null +++ b/website/templates/shortcodes/finale.html @@ -0,0 +1,17 @@ +{# Closing banner: the Bug surrenders, the loop continues. + Inline shortcode. Args: title="closing line", alt="scene description" #} +
+
+
+ + {% if alt %}
{{ alt | e }}
{% endif %} +
+

{{ title | e }}

+ +
+
diff --git a/website/templates/shortcodes/loop_diagram.html b/website/templates/shortcodes/loop_diagram.html new file mode 100644 index 0000000..0d62e92 --- /dev/null +++ b/website/templates/shortcodes/loop_diagram.html @@ -0,0 +1,3 @@ +{# The infinity loop diagram with linked phase nodes. + Inline shortcode. Args: caption="text under the diagram" #} +{% include "partials/svg/loop.html" %} diff --git a/website/templates/shortcodes/member.html b/website/templates/shortcodes/member.html new file mode 100644 index 0000000..906465c --- /dev/null +++ b/website/templates/shortcodes/member.html @@ -0,0 +1,13 @@ +{# One team member row: portrait + name + intro. + Block shortcode. Args: char="char-dev"..., pose, title="Dev" #} +
+
+ +
+
+

{{ title | e }}

+ {{ body | markdownify | safe }} +
+
diff --git a/website/templates/shortcodes/panel.html b/website/templates/shortcodes/panel.html new file mode 100644 index 0000000..8484803 --- /dev/null +++ b/website/templates/shortcodes/panel.html @@ -0,0 +1,5 @@ +{# One comic panel. Block shortcode. + Args: tint="sec"|"warm", halftone=true, center=true #} +
+{{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/resources.html b/website/templates/shortcodes/resources.html new file mode 100644 index 0000000..5ed6988 --- /dev/null +++ b/website/templates/shortcodes/resources.html @@ -0,0 +1,5 @@ +{# Curated resource links as cards. Block shortcode wrapping a markdown + list of links. #} +
+{{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/scene.html b/website/templates/shortcodes/scene.html new file mode 100644 index 0000000..3224851 --- /dev/null +++ b/website/templates/shortcodes/scene.html @@ -0,0 +1,65 @@ +{# Embeds a character scene from the inline SVG library. + Inline shortcode. Args: + name="char-dev"|"char-sec"|"char-ops"|"char-bug"|"trio" + pose (see each character partial) + alt "described action" (screen-reader text) + size "lg"|"sm" + bg "board"|"window"|"gears"|"radar" (muted backdrop; character scales down) + Backdrops use only faint tokens so the character stays the focus. #} +{% set n = name | default(value="char-dev") %} +
+ {% if n == "trio" %} + {% include "partials/svg/trio.html" %} + {% else %} + + {% endif %} + {% if alt %}
{{ alt | e }}
{% endif %} +
diff --git a/website/templates/shortcodes/strip.html b/website/templates/shortcodes/strip.html new file mode 100644 index 0000000..eb5e58a --- /dev/null +++ b/website/templates/shortcodes/strip.html @@ -0,0 +1,5 @@ +{# A webtoon strip: vertical column of panels, optionally side by side on + wide screens. Block shortcode. Args: row=true #} +
+{{ body | markdownify | safe }} +
diff --git a/website/templates/shortcodes/team.html b/website/templates/shortcodes/team.html new file mode 100644 index 0000000..ac4cb76 --- /dev/null +++ b/website/templates/shortcodes/team.html @@ -0,0 +1,5 @@ +{# Asymmetric team layout: first panel wide, second panel offset. + Block shortcode wrapping panel shortcodes. #} +
+{{ body | markdownify | safe }} +
diff --git a/website/templates/tools.html b/website/templates/tools.html new file mode 100644 index 0000000..5247e7d --- /dev/null +++ b/website/templates/tools.html @@ -0,0 +1,40 @@ +{% include "header.html" %} +
+
+

{{ page.title | e }}

+
+ {{ content }} +
+ {# Filter chips are JS-only; without JS every section stays visible. + Phases listed here are the ones that have tools in data/tools.yml. #} +
+ + {% for ph in ["design", "build", "test", "operate"] %} + + {% endfor %} +
+
+ {% set tools = load_data(path="data/tools.yml") %} + {% for ph in ["design", "build", "test", "operate"] %} +
+
+ {% set icon = ph %}{% include "partials/svg/icon.html" %} +

{{ ("nav." ~ ph) | t }}

+
+
    + {% for tool in tools %}{% if tool.phase == ph %} +
  • +
    + {{ tool.name | e }} + {{ tool.category | e }} +
    +

    {% if page.language == "ko" and tool.description_ko %}{{ tool.description_ko | e }}{% elif page.language == "ja" and tool.description_ja %}{{ tool.description_ja | e }}{% else %}{{ tool.description | e }}{% endif %}

    +
  • + {% endif %}{% endfor %} +
+
+ {% endfor %} +
+
+
+{% include "footer.html" %}