Skip to content

Commit 8f6e0df

Browse files
committed
Snapshot local work before sync
1 parent 8feca6b commit 8f6e0df

5 files changed

Lines changed: 278 additions & 42 deletions

File tree

.github/workflows/casket-pages.yml

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
name: GitHub Pages
3+
4+
on:
5+
push:
6+
branches: [main, master]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
pages: write
12+
id-token: write
13+
14+
concurrency:
15+
group: "pages"
16+
cancel-in-progress: false
17+
18+
jobs:
19+
build:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
24+
25+
- name: Checkout casket-ssg
26+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
27+
with:
28+
repository: hyperpolymath/casket-ssg
29+
path: .casket-ssg
30+
31+
- name: Setup GHCup
32+
uses: haskell-actions/setup@ec49483bfc012387b227434aba94f59a6ecd0900 # v2
33+
with:
34+
ghc-version: '9.8.2'
35+
cabal-version: '3.10'
36+
37+
- name: Cache Cabal
38+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
39+
with:
40+
path: |
41+
~/.cabal/packages
42+
~/.cabal/store
43+
.casket-ssg/dist-newstyle
44+
key: ${{ runner.os }}-casket-${{ hashFiles('.casket-ssg/casket-ssg.cabal') }}
45+
46+
- name: Build casket-ssg
47+
working-directory: .casket-ssg
48+
run: cabal build
49+
50+
- name: Prepare site source
51+
shell: bash
52+
run: |
53+
set -euo pipefail
54+
rm -rf .site-src _site
55+
56+
if [ -d site ]; then
57+
cp -R site .site-src
58+
else
59+
mkdir -p .site-src
60+
TODAY="$(date +%Y-%m-%d)"
61+
REPO_NAME="${{ github.event.repository.name }}"
62+
REPO_URL="https://github.com/${{ github.repository }}"
63+
README_URL=""
64+
65+
if [ -f README.md ]; then
66+
README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.md"
67+
elif [ -f README.adoc ]; then
68+
README_URL="${REPO_URL}/blob/${{ github.ref_name }}/README.adoc"
69+
fi
70+
71+
{
72+
echo "---"
73+
echo "title: ${REPO_NAME}"
74+
echo "date: ${TODAY}"
75+
echo "---"
76+
echo
77+
echo "# ${REPO_NAME}"
78+
echo
79+
echo "Static documentation site for ${REPO_NAME}."
80+
echo
81+
echo "- Source repository: [${{ github.repository }}](${REPO_URL})"
82+
if [ -n "${README_URL}" ]; then
83+
echo "- README: [project README](${README_URL})"
84+
fi
85+
if [ -d docs ]; then
86+
echo "- Docs directory: [docs/](${REPO_URL}/tree/${{ github.ref_name }}/docs)"
87+
fi
88+
echo
89+
echo "Project-specific site content can be added later under site/."
90+
} > .site-src/index.md
91+
fi
92+
93+
- name: Build site
94+
run: |
95+
mkdir -p _site
96+
cd .casket-ssg && cabal run casket-ssg -- build ../.site-src ../_site
97+
touch ../_site/.nojekyll
98+
99+
- name: Setup Pages
100+
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
101+
102+
- name: Upload artifact
103+
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
104+
with:
105+
path: '_site'
106+
107+
deploy:
108+
environment:
109+
name: github-pages
110+
url: ${{ steps.deployment.outputs.page_url }}
111+
runs-on: ubuntu-latest
112+
needs: build
113+
steps:
114+
- name: Deploy to GitHub Pages
115+
id: deployment
116+
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4

ROADMAP.adoc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
= Project Wharf Roadmap
3+
4+
== Current Status
5+
6+
Project Wharf is the strongest architectural core in this portfolio, but it should be judged by trusted-core rigor rather than by broad completion percentages.
7+
8+
* The offline-controller / online-runtime split is coherent and strategically strong.
9+
* The database policy engine, integrity path, and mooring model are the highest-value differentiators.
10+
* The release story still depends on deeper fail-closed verification and clearer crypto-release posture.
11+
12+
== P0 Release Blockers
13+
14+
* [ ] Prove and test fail-closed handling for unrecognized or administrative SQL statements across the DB policy path.
15+
* [ ] Expand database-policy tests beyond smoke coverage so the trusted core is exercised more realistically.
16+
* [ ] Resolve the Ed448 audit dependency or standardize deployment guidance around the audited ML-DSA-only path.
17+
* [ ] Reconcile the 95%-complete style language with the thin current test surface.
18+
19+
== P1 Hardening Work
20+
21+
* [ ] Add load, fault-injection, and concurrency testing for mooring, integrity verification, and DB proxying.
22+
* [ ] Add explicit release criteria for the trusted core.
23+
* [ ] Publish a tighter operator guide for what is supported, experimental, or crypto-audit dependent.
24+
25+
== P2 Productization
26+
27+
* [ ] Treat Wharf as the backbone of the live stack once the trusted core is proven.
28+
* [ ] Use WordPress deployments as real operational validation instead of broad architectural percentages.
29+
* [ ] Keep the scope centered on runtime hardening rather than accumulating adjacent infrastructure features too early.

TOPOLOGY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
22
# TOPOLOGY.md - Project Wharf
33

4+
> NOTE (2026-03-30): Read this dashboard as an architectural tracking view, not as a release gate by itself. For current blockers and deployment posture, use `ROADMAP.adoc` as the authoritative readiness document.
5+
46
## System Architecture
57

68
```

crates/wharf-core/src/db_policy.rs

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
//! - **Immutable (Red)**: Read-only, writes blocked unless from Wharf (e.g., wp_users, wp_options)
1515
//! - **Hybrid (Grey)**: Conditional based on specific columns/values (e.g., transient caches in wp_options)
1616
17+
use serde::{Deserialize, Serialize};
1718
use sqlparser::ast::{Statement, TableFactor};
1819
use sqlparser::dialect::MySqlDialect;
1920
use sqlparser::parser::Parser;
20-
use serde::{Deserialize, Serialize};
2121
use thiserror::Error;
2222

2323
#[derive(Error, Debug)]
@@ -110,6 +110,9 @@ impl PolicyEngine {
110110

111111
for statement in ast {
112112
match &statement {
113+
Statement::Query(_) => {
114+
continue;
115+
}
113116
Statement::Insert { table_name, .. } => {
114117
let table = table_name.to_string();
115118
return self.check_write_permission(&table);
@@ -132,8 +135,14 @@ impl PolicyEngine {
132135
// ALTER is always blocked from the yacht
133136
return Ok(QueryAction::Block);
134137
}
135-
// SELECT and other read operations are always allowed
136-
_ => {}
138+
_ => {
139+
if self.is_safe_session_or_read_query(sql) {
140+
continue;
141+
}
142+
143+
// Fail closed for anything we do not explicitly understand.
144+
return Ok(QueryAction::Block);
145+
}
137146
}
138147
}
139148

@@ -145,12 +154,22 @@ impl PolicyEngine {
145154
let normalized = table.trim_matches('`').to_lowercase();
146155

147156
// Check if explicitly allowed (exact match, not substring)
148-
if self.policy.allow_write.iter().any(|t| normalized == t.to_lowercase()) {
157+
if self
158+
.policy
159+
.allow_write
160+
.iter()
161+
.any(|t| normalized == t.to_lowercase())
162+
{
149163
return Ok(QueryAction::Allow);
150164
}
151165

152166
// Check if explicitly locked (exact match, not substring)
153-
if self.policy.lock_down.iter().any(|t| normalized == t.to_lowercase()) {
167+
if self
168+
.policy
169+
.lock_down
170+
.iter()
171+
.any(|t| normalized == t.to_lowercase())
172+
{
154173
return Err(PolicyError::ImmutableTableViolation { table: normalized });
155174
}
156175

@@ -180,10 +199,10 @@ impl PolicyEngine {
180199
fn extract_all_tables_from_factor(&self, factor: &TableFactor) -> Vec<String> {
181200
match factor {
182201
TableFactor::Table { name, .. } => vec![name.to_string()],
183-
TableFactor::Derived { subquery, .. } => {
184-
self.extract_tables_from_query(subquery)
185-
}
186-
TableFactor::NestedJoin { table_with_joins, .. } => {
202+
TableFactor::Derived { subquery, .. } => self.extract_tables_from_query(subquery),
203+
TableFactor::NestedJoin {
204+
table_with_joins, ..
205+
} => {
187206
let mut tables = self.extract_all_tables_from_factor(&table_with_joins.relation);
188207
for join in &table_with_joins.joins {
189208
tables.extend(self.extract_all_tables_from_factor(&join.relation));
@@ -207,6 +226,29 @@ impl PolicyEngine {
207226
}
208227
tables
209228
}
229+
230+
fn is_safe_session_or_read_query(&self, sql: &str) -> bool {
231+
let normalized = sql.trim_start().to_uppercase();
232+
let safe_prefixes = [
233+
"SELECT ",
234+
"WITH ",
235+
"SHOW ",
236+
"DESCRIBE ",
237+
"DESC ",
238+
"EXPLAIN ",
239+
"SET NAMES ",
240+
"SET CHARACTER SET ",
241+
"SET SESSION ",
242+
"START TRANSACTION",
243+
"BEGIN",
244+
"COMMIT",
245+
"ROLLBACK",
246+
];
247+
248+
safe_prefixes
249+
.iter()
250+
.any(|prefix| normalized.starts_with(prefix))
251+
}
210252
}
211253

212254
#[cfg(test)]
@@ -223,7 +265,9 @@ mod tests {
223265
#[test]
224266
fn test_insert_to_comments_allowed() {
225267
let engine = PolicyEngine::new(DatabasePolicy::default());
226-
let result = engine.analyze("INSERT INTO wp_comments (comment_content) VALUES ('test')").unwrap();
268+
let result = engine
269+
.analyze("INSERT INTO wp_comments (comment_content) VALUES ('test')")
270+
.unwrap();
227271
assert_eq!(result, QueryAction::Allow);
228272
}
229273

@@ -237,7 +281,9 @@ mod tests {
237281
#[test]
238282
fn test_unknown_table_blocked_fail_closed() {
239283
let engine = PolicyEngine::new(DatabasePolicy::default());
240-
let result = engine.analyze("INSERT INTO wp_custom_table (col) VALUES ('test')").unwrap();
284+
let result = engine
285+
.analyze("INSERT INTO wp_custom_table (col) VALUES ('test')")
286+
.unwrap();
241287
assert_eq!(result, QueryAction::Block);
242288
}
243289

@@ -251,29 +297,51 @@ mod tests {
251297
#[test]
252298
fn test_alter_always_blocked() {
253299
let engine = PolicyEngine::new(DatabasePolicy::default());
254-
let result = engine.analyze("ALTER TABLE wp_comments ADD COLUMN backdoor TEXT").unwrap();
300+
let result = engine
301+
.analyze("ALTER TABLE wp_comments ADD COLUMN backdoor TEXT")
302+
.unwrap();
255303
assert_eq!(result, QueryAction::Block);
256304
}
257305

306+
#[test]
307+
fn test_truncate_blocked_fail_closed() {
308+
let engine = PolicyEngine::new(DatabasePolicy::default());
309+
let result = engine.analyze("TRUNCATE TABLE wp_posts").unwrap();
310+
assert_eq!(result, QueryAction::Block);
311+
}
312+
313+
#[test]
314+
fn test_session_queries_allowed() {
315+
let engine = PolicyEngine::new(DatabasePolicy::default());
316+
let result = engine.analyze("SET NAMES utf8mb4").unwrap();
317+
assert_eq!(result, QueryAction::Allow);
318+
}
319+
258320
#[test]
259321
fn test_exact_table_match_no_substring() {
260322
// wp_comments_backdoor should NOT match wp_comments (exact match only)
261323
let engine = PolicyEngine::new(DatabasePolicy::default());
262-
let result = engine.analyze("INSERT INTO wp_comments_backdoor (col) VALUES ('test')").unwrap();
324+
let result = engine
325+
.analyze("INSERT INTO wp_comments_backdoor (col) VALUES ('test')")
326+
.unwrap();
263327
assert_eq!(result, QueryAction::Block);
264328
}
265329

266330
#[test]
267331
fn test_delete_from_comments_allowed() {
268332
let engine = PolicyEngine::new(DatabasePolicy::default());
269-
let result = engine.analyze("DELETE FROM wp_comments WHERE comment_id = 1").unwrap();
333+
let result = engine
334+
.analyze("DELETE FROM wp_comments WHERE comment_id = 1")
335+
.unwrap();
270336
assert_eq!(result, QueryAction::Allow);
271337
}
272338

273339
#[test]
274340
fn test_update_comments_allowed() {
275341
let engine = PolicyEngine::new(DatabasePolicy::default());
276-
let result = engine.analyze("UPDATE wp_comments SET comment_content = 'edited' WHERE comment_id = 1").unwrap();
342+
let result = engine
343+
.analyze("UPDATE wp_comments SET comment_content = 'edited' WHERE comment_id = 1")
344+
.unwrap();
277345
assert_eq!(result, QueryAction::Allow);
278346
}
279347

0 commit comments

Comments
 (0)