Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f3c6f30a9 | ||
|
|
2eb5d74f59 | ||
|
|
bef1b99c78 | ||
|
|
5980e2caba | ||
|
|
27a6d98d0c | ||
|
|
59f169e32d | ||
|
|
1a5338e876 | ||
|
|
bbb163c25c | ||
|
|
2c7a3b8465 | ||
|
|
d37538264a | ||
|
|
cd5eeafa08 | ||
|
|
4ef2b5333a | ||
|
|
3d2ec5869b | ||
|
|
6cabe6796a | ||
|
|
ce479a2c51 | ||
|
|
bc27894b38 | ||
|
|
f38b5d795c | ||
|
|
67cf263ec5 | ||
|
|
db969f51b4 | ||
|
|
8d4a82f19a | ||
|
|
cb072bf60a | ||
|
|
365b178496 | ||
|
|
b75e0bb38e | ||
|
|
b61ec07c87 | ||
|
|
6469ad00a3 | ||
|
|
ed20e0b982 | ||
|
|
42cf023efb | ||
|
|
ae3f5128c2 | ||
|
|
bc44d4cf73 | ||
|
|
838bc7a96f | ||
|
|
53c0de0960 | ||
|
|
549ecc36aa | ||
|
|
463de81ffa | ||
|
|
79d2e887d3 | ||
|
|
1119645a00 | ||
|
|
a1513b871e | ||
|
|
59ff2b13fc | ||
|
|
7d950cbdbe | ||
|
|
66d5fecb0e | ||
|
|
0b18658686 | ||
|
|
bbd5793d8b | ||
|
|
0bda92b091 | ||
|
|
bf9fca1b9a | ||
|
|
d542ac442f | ||
|
|
0ee0b4ea7c | ||
|
|
2fe1bc2eea | ||
|
|
3e39339332 | ||
|
|
6d554930be | ||
|
|
a56c2f8d48 | ||
|
|
27ff40b739 | ||
|
|
4937edd7f4 | ||
|
|
fcba390804 | ||
|
|
8b6e3e60bc | ||
|
|
d244a34fc9 | ||
|
|
686a745f0b | ||
|
|
510947d173 | ||
|
|
589a745c5b | ||
|
|
d04c55c731 | ||
|
|
a6a6abf009 | ||
|
|
88368fa31d | ||
|
|
fdb1db216d | ||
|
|
6d833f9654 | ||
|
|
6bcd0adf81 | ||
|
|
93348ad604 | ||
|
|
7561bb0c39 | ||
|
|
959a9ffab4 | ||
|
|
2348575737 | ||
|
|
b2aac21b90 | ||
|
|
438b1e93d7 | ||
|
|
674737f840 | ||
|
|
150be89246 |
No files matched your search
@@ -4,6 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# One deploy at a time: `surfer deploy` swaps the site atomically, so two runs
|
||||
# racing each other is not corruption, but it is a wasted build and the last
|
||||
# swap wins.
|
||||
concurrency:
|
||||
group: surfer-datacontroller-io
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
Build-and-publish:
|
||||
@@ -18,8 +26,44 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
# Blocking gate. `npm audit --audit-level=<x>` is all-or-nothing and this
|
||||
# tree cannot pass a flat gate at any level: the Gatsby 5 build toolchain
|
||||
# carries ~100 advisories with no non-breaking fix, so gating on the tree
|
||||
# as a whole stops publishing permanently. scripts/audit-gate.mjs instead
|
||||
# accepts the known set (scripts/audit-baseline.json, with an owner and a
|
||||
# reason) and FAILS on any new critical/high advisory.
|
||||
- name: Audit for new critical/high advisories
|
||||
run: node scripts/audit-gate.mjs
|
||||
|
||||
# The prettier and type checks used to live in a GitHub workflow that
|
||||
# never ran on this forge; keep them ahead of the build so a type or
|
||||
# formatting error blocks the deploy rather than shipping.
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Check prettier offences
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
# Deploy tooling pinned to an exact version: the workflow publishes
|
||||
# whatever is committed, and an unpinned `latest` would let a breaking or
|
||||
# compromised upstream release ship straight to the live site.
|
||||
#
|
||||
# `deploy` (v7) replaces the site atomically - it extracts to a staging
|
||||
# folder and swaps it in, so a failed upload leaves the current site
|
||||
# serving. The v6 `put` this replaces was token authenticated and is
|
||||
# gone; v7 takes the Cloudron username plus an app password for this app.
|
||||
- name: Publish
|
||||
run: npx surfer put --token ${{ secrets.SURFER_TOKEN }} --server datacontroller.io public/* /
|
||||
env:
|
||||
SURFER_PASSWORD: ${{ secrets.SURFER_PASSWORD }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
COMMIT_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npx --yes @cloudron/surfer@7.2.0 deploy public \
|
||||
--server datacontroller.io \
|
||||
--username allan \
|
||||
--password "$SURFER_PASSWORD" \
|
||||
--message "Build from ${REF_NAME} @ ${COMMIT_SHA:0:8}"
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Gatsby Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js from .nvmrc
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Check prettier offences
|
||||
run: npm run lint
|
||||
|
||||
- uses: enriikke/gatsby-gh-pages-action@v2
|
||||
with:
|
||||
access-token: ${{ secrets.DCSITE }}
|
||||
deploy-branch: gh-pages
|
||||
gatsby-args: --prefix-paths
|
||||
@@ -0,0 +1,12 @@
|
||||
# Secret scanning for staged content, so a credential is caught before it is
|
||||
# committed rather than after it has been pushed and published.
|
||||
#
|
||||
# gitleaks is pinned to an exact tag: a floating rev would pull whatever the
|
||||
# hook's upstream does next into every contributor's commit path.
|
||||
#
|
||||
# Install once per clone: `pre-commit install`.
|
||||
repos:
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.30.1
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: 'Derived Variables: computed columns in the EDIT screen'
|
||||
description: Data Controller can now compute columns that do not exist in the table - and, crucially, filter on them. Configured per table in MPE_DERIVED, computed on every read, never stored.
|
||||
date: '2026-10-21 12:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
previewImg: './derived-grid.png'
|
||||
tags:
|
||||
- Data Controller
|
||||
- Features
|
||||
---
|
||||
|
||||
# Derived variables: computed columns you can filter on
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: a column in the EDIT screen that is not in the table. A region worked out from a state code. A line value that is quantity times price. Not stored, not loaded, just *there* - and, as the requester usually adds, filterable.
|
||||
|
||||
The "not stored" part is easy. The "filterable" part is where the design lives, and it is worth writing down, because it explains every other decision in the feature.
|
||||
|
||||
## Why a computed column is not automatically filterable
|
||||
|
||||
You would write it in a DATA step:
|
||||
|
||||
```sas
|
||||
data out;
|
||||
set mylib.mytable;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
where REGION = 'West';
|
||||
run;
|
||||
```
|
||||
|
||||
That fails. A `WHERE` statement applies to the data set named on the `SET` statement - the *input* - and on that input `REGION` does not exist. SAS cannot see a variable that the same step creates later. You can put the assignment and the filter in separate steps, but then the filter runs after the step has read every row, which is precisely the work the filter exists to avoid - and for a large table that is the difference between a fast screen and a coffee break.
|
||||
|
||||
So the calculation has to happen somewhere the filter can see it. In Data Controller it happens in a **DATA step view**:
|
||||
|
||||
```sas
|
||||
data work.vw_derived / view=work.vw_derived;
|
||||
set mylib.mytable;
|
||||
attrib REGION length=$8 label='REGION=ifc(...)' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
run;
|
||||
|
||||
data out;
|
||||
set work.vw_derived;
|
||||
where %inc filtref;;
|
||||
run;
|
||||
```
|
||||
|
||||
The view is not executed when it is defined. When the second step reads it, the assignment runs as part of the view's step, so by the time the `WHERE` is evaluated `REGION` is a column of the input. The filter is unchanged, the row-level security clause is unchanged, and nothing extra is read.
|
||||
|
||||
## What it looks like
|
||||
|
||||
The demo table below has four rows. `LINE_VALUE` is `QTY * PRICE`, `REGION` is derived from `STATE`, and `REGION_LABEL` is built on `REGION`. None of the three is a column of the table, and all three are read-only in the grid - and marked so, with a tinted header carrying an `fx` badge, italic tinted cells, and an edge where the computed block begins, so that "computed" never has to be confused with "locked by a rule":
|
||||
|
||||

|
||||
|
||||
Because they are on the input to the filter, they filter like any other column. Here is `REGION = 'West'`, with the filter bar naming the derived variable:
|
||||
|
||||

|
||||
|
||||
and a numeric one, `LINE_VALUE > 50`:
|
||||
|
||||

|
||||
|
||||
The value list in the filter dialog is built the same way, from the table under the rest of the filter - so a derived column gets a dropdown of its distinct values, not a blank box.
|
||||
|
||||
## Configuration
|
||||
|
||||
`MPE_DERIVED` is itself a Data Controller table, so it is maintained through the EDIT screen with the usual submit / review / approve workflow. One row per variable - these are the three rows behind the screenshots above:
|
||||
|
||||

|
||||
|
||||
|
||||
```
|
||||
VAR_NM REGION
|
||||
VAR_DEPS STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 8
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL (blank)
|
||||
VAR_LOGIC ifc(STATE in ('CA','OR','WA'),'West','East')
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
and the chained one:
|
||||
|
||||
```
|
||||
VAR_NM REGION_LABEL
|
||||
VAR_DEPS REGION STATE
|
||||
VAR_TYPE C
|
||||
VAR_LEN 14
|
||||
VAR_FMT (blank)
|
||||
VAR_LABEL Region and state
|
||||
VAR_LOGIC cats(REGION,'-',STATE)
|
||||
ACTIVE 1
|
||||
```
|
||||
|
||||
Which produces, at read time:
|
||||
|
||||
```sas
|
||||
attrib REGION length=$8 label='REGION=ifc(STATE in (''CA'',''OR'',''WA''),''West'',''East'')' format=$8.;
|
||||
REGION = ifc(STATE in ('CA','OR','WA'),'West','East');
|
||||
attrib REGION_LABEL length=$14 label='Region and state' format=$14.;
|
||||
REGION_LABEL = cats(REGION,'-',STATE);
|
||||
```
|
||||
|
||||
Type and length are declared rather than guessed, so a value is truncated exactly as specified. The format is optional; when it is left blank a character variable gets `$<length>.` and a numeric one `<length>.`, and a date, time or datetime format makes the column render - and filter - as a date, the same way a stored column does. The label is `VAR_LABEL` when you supply one, otherwise the name, an equals sign and the expression, so a user who switches labels on can see where the value came from.
|
||||
|
||||
## Dependencies, without a sequence number
|
||||
|
||||
`REGION_LABEL` needs `REGION` to exist first. Rather than maintain a sort order, each row declares its inputs in `VAR_DEPS`, and the statements are emitted in dependency order. Adding a variable in the middle of a chain means editing one row's `VAR_DEPS`, not renumbering the table.
|
||||
|
||||
`VAR_DEPS` lists *every* input - physical columns and derived variables alike - and the declaration is checked against the expression in both directions: a derived variable the expression references must be declared, and a declared name the expression does not reference is an error. A declared name must be a column of the table or a derived variable of the same table, and a cycle is rejected with the path that forms it:
|
||||
|
||||
```
|
||||
VAR_DEPS contains a cycle: A -> B -> C -> A
|
||||
```
|
||||
|
||||
## What it will not do
|
||||
|
||||
**Expressions only.** `VAR_LOGIC` must be a single SAS expression. Semicolons, macro triggers (`%`, `&`) and comments are rejected, so one row can never be more than an expression - which is the point: a config table is not a place to write programs. Conditional logic is available through functions such as `ifc()`, `choosec()`, `cats()`, `catx()`, `put()`, `input()` and `intnx()`.
|
||||
|
||||
**No shadowing, no self-reference.** A derived variable may not reuse the name of a column in the target table. Nor may it reference itself: `SUMMER = SUMMER + 1` implies a retained value, which depends on the order rows happen to arrive in.
|
||||
|
||||
**Never stored.** Derived values are computed on every read. Nothing derived is submitted, reviewed or written back - the approval package contains physical columns only, and a crafted submission that includes a derived column is discarded before the security check runs.
|
||||
|
||||
**EDIT screen only, for now.** The VIEW screen and the download services do not show derived columns. A saved filter that references one still applies there and is evaluated the same way.
|
||||
|
||||
**Not for row level security.** A rule in `MPE_ROW_LEVEL_SECURITY` cannot reference a derived column: the rule is evaluated against the submitted data, where the column does not exist. Saving a rule that names one is rejected with an explanation rather than left to fail at submit time.
|
||||
|
||||
## One warning
|
||||
|
||||
An expression can reference a column that is hidden from a user by [Column Level Security](https://docs.datacontroller.io/column-level-security/) - and the derived column will show the result. Configure derived variables with the same care as any other data exposure.
|
||||
|
||||
## Availability
|
||||
|
||||
Derived variables are available on SAS 9, Viya and SASjs Server. The configuration table, the settings and the post-edit validation are created by the usual upgrade - there is nothing to install beyond the release itself.
|
||||
|
||||
The full reference, including every column of `MPE_DERIVED` and the validation rules applied at submit time, is in the [docs](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
@@ -61,7 +61,7 @@ Did you know that, in addition to a regular missing value in SAS (`.`), there ar
|
||||
|
||||
These values can now be both viewed and edited in Data Controller following an update to the [SASjs Adapter](https://github.com/sasjs/adapter#variable-types).
|
||||
|
||||
`video: [Retain Formulas when Loading Excel to SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
||||
`video: [Managing Special Missing Values with Data Controller for SAS](https://www.youtube-nocookie.com/embed/ggrcNr23Jzw)`
|
||||
|
||||
There is nothing extra to configure for special SAS numerics - they are simply available by default, for numeric cells.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: 'The column that is not in the table'
|
||||
description: 'Derived variables give the EDIT screen columns that do not exist in the table - computed on every read, never stored, and filterable like any other column. The full story is in the article.'
|
||||
date: '2026-10-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
- Features
|
||||
previewImg: './derived-fx.png'
|
||||
---
|
||||
|
||||
# The column that is not in the table
|
||||
|
||||
Every reporting team has a version of this request, and it always sounds like a five minute job: "can we have a region column? It is just the state, mapped. And a line value - quantity times price. We do not need it stored, we just need it on the screen."
|
||||
|
||||
It is never a five minute job - so Data Controller now does the obvious thing instead. You write the expression, and the EDIT screen shows the column: computed on every read, never stored, marked `fx` and read-only, so nobody has to wonder whether it is locked or simply has nothing to edit.
|
||||
|
||||
The part that took the thinking is that they filter like any other column. That needed a DATA step view rather than a data step, and the reasoning is worth reading: [Derived variables: computed columns you can filter on](/derived-variables/).
|
||||
|
||||
Reference: [Derived Variables](https://docs.datacontroller.io/derived-variables/).
|
||||
|
||||
If a table of yours has been waiting for "just one more column", we would love to [hear from you](/contact).
|
||||
|
||||
<!--
|
||||
Feed teaser for the article at /derived-variables/. The article carries the
|
||||
engineering - why a computed column is not automatically filterable, the DATA
|
||||
step view, the configuration table, the limits - so this post links to it
|
||||
rather than repeating it.
|
||||
|
||||
Image prompt (cover): a clean, modern illustration on a white background: a
|
||||
spreadsheet grid rendered in soft slate and light blue, with the leftmost four
|
||||
columns showing plain data and three further columns floating slightly to the
|
||||
right, detached from the table, tinted light blue and italicised, each marked
|
||||
with a small "fx" badge. A thin left border separates the floating block from
|
||||
the stored columns. No text other than the fx badges. Flat vector style, brand
|
||||
palette: slate #314351, light blue #d3e5f2, accent green #79a843 used sparingly
|
||||
as a single dot.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,55 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - a Customer Story'
|
||||
description: Data Controller for SAS® lets business users self serve their data updates - restricted access to the data values, with the data model protected.
|
||||
date: '2026-10-11 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './customer-story.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - a Customer Story
|
||||
|
||||
Data Controller for SAS® empowers business users to self serve their data updates, by providing restricted access to the data *values* whilst protecting the data *model*. This short customer story shows what that looks like for Data and Analytics teams in regulated industries.
|
||||
|
||||
The distinction is the whole point. An editor can add, correct and delete rows in the tables they are responsible for, but they cannot change the structure underneath - no new columns, no altered formats, no dropped keys. The shape of the table is fixed, and only its contents move. Every change is staged, validated at the point of entry, reviewed as a diff and approved by a data owner before it is loaded, with a full audit trail of who changed what, when and why.
|
||||
|
||||
See it in action:
|
||||
|
||||
<iframe title="Data Controller for SAS - a Customer Story" width="560" height="315" src="https://vid.4gl.io/videos/embed/kpSGoNE26FSif8X9A6pUfe" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
Would you like to see how this would work against your own tables? [Get in touch](/contact) to arrange a walkthrough.
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Business users need to keep their data current. They do not need to change the tables underneath.
|
||||
|
||||
Most tools blur the two. Hand someone a spreadsheet and they reshape the file. Grant write access to a production table and they can reshape the table. Neither is what you want.
|
||||
|
||||
Data Controller for SAS® draws the line at the data VALUES. Business users self serve their updates - adding, correcting and deleting rows in the tables they own - with restricted access to what is inside the table, whilst the data MODEL stays protected. No new columns, no altered formats, no dropped keys.
|
||||
|
||||
→ Every change is staged and validated at the point of entry
|
||||
→ Reviewed as a diff and approved by a data owner before it loads
|
||||
→ With a full audit trail of who changed what, when and why
|
||||
|
||||
A short customer story, for Data and Analytics teams in regulated industries:
|
||||
|
||||
✅ Watch the story, then book a Discovery Call - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #dataquality
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: a single data table drawn as two separated layers. The top layer is the table's structure - a header band of column blocks in muted slate, held behind a closed green padlock on the left. The bottom layer is the table's values - four rows of rounded cells in lighter slate, with two cells filled solid brand green and a small green pencil resting beside the lower right of the grid, as if those values are the ones being edited. A thin vertical gap separates the two layers, so the padlock reads as protecting the structure above while the open cells below are the editable values.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./customer-story.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: 'Full Table Search: Find Any Value in Any Table'
|
||||
description: Type a value into the search box and Data Controller scans every column for it - no query, no WHERE clause.
|
||||
date: '2026-09-15 17:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './full-table-search.jpeg'
|
||||
---
|
||||
|
||||
# Full Table Search: Find Any Value in Any Table
|
||||
|
||||
Finding a value in a large table usually means writing a query first: guess which column it lives in, write a WHERE clause, run it, and try again when you guess wrong. Data Controller's **full table search** removes that step. Choose a library and table in the Viewer, type a value into the search box and press Enter - every column in the table is scanned for it, and the matching rows come straight back into the grid.
|
||||
|
||||
## How it works
|
||||
|
||||
The search box sits in the Viewer toolbar, next to a **Numeric** checkbox:
|
||||
|
||||
- **Text search** matches part of a value using the case sensitive SAS® `CONTAINS` operator, so `smith` finds `Goldsmith` but not `Smithson`.
|
||||
- **Numeric search** (tick the box) matches the number exactly against every numeric column in the table.
|
||||
|
||||
Whichever you use, the results are ordinary rows in the Viewer, with the rest of the screen behaving exactly as it does for a normal view.
|
||||
|
||||
## Any database, not just SAS datasets
|
||||
|
||||
The scan runs inside SAS, against whichever libname engine the table is assigned to - so it is not limited to SAS datasets.
|
||||
|
||||
## Built on open source
|
||||
|
||||
This feature, like most of Data Controller, is built on the [SASjs Macro Core](https://github.com/sasjs/core) library, using the [`%mp_searchdata()`](https://core.sasjs.io/mp__searchdata_8sas.html) macro. The macro assembles a single DATA step with one `OR` clause per column - a `CONTAINS` test for character columns, an equality test for numeric ones - and writes out only the matching records. It is MIT licensed, so you can read, test and audit the code that is running against your data.
|
||||
|
||||
A few things worth knowing:
|
||||
|
||||
- The search respects your current filter, Row Level Security and Column Level Security - it scans the view you are entitled to see, not the raw table.
|
||||
- Full table search is available in ViewBoxes as well, so the related tables lined up beside your main grid can be searched the same way.
|
||||
- Nothing is shipped to an external index or search service: the scan happens on your own SAS platform.
|
||||
|
||||
See it in action:
|
||||
|
||||
<iframe title="Full Table Search" width="560" height="315" src="https://vid.4gl.io/videos/embed/qdEv4PP2oiPr5VXwLbN58B" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
|
||||
|
||||
More detail in the [Viewer documentation](https://docs.datacontroller.io/dcu-tableviewer/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Controller for SAS® has a plethora of features to make Data Discovery easier
|
||||
|
||||
Here we demonstrate "full table search". No need to formulate a query - just type a value and hit enter!
|
||||
|
||||
It works on all databases
|
||||
|
||||
This feature, like most others, is built on our open-source #SASjs library - using the `mp_searchdata()` macro (https://lnkd.in/gxZZq76j).
|
||||
|
||||
#sas #sasapps #sasviya
|
||||
|
||||
video: https://vid.4gl.io/w/qdEv4PP2oiPr5VXwLbN58B
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Flat vector illustration on a dark slate (#314351) background with a faint dot grid: a stylised data table across the lower half (rounded header bar plus eight rows of rounded cells), with the fourth row highlighted in brand green (#90c445). A large magnifying glass with a thick green rim and diagonal handle overlaps the table on the right, and inside its dark lens the highlighted row appears magnified with two white value blocks. Headline "FULL TABLE SEARCH" in bold white uppercase, subheading "any value, every column - no query required" in brand green. 1200x627 landscape, no other text. Composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: 'If: An Ode to Data Control'
|
||||
description: Rudyard Kipling's 'If', reworked for the data governance era - the full text of a poem first published in 2022.
|
||||
date: '2026-09-20 15:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './if-an-ode-to-data-control.jpeg'
|
||||
---
|
||||
|
||||
# If: An Ode to Data Control
|
||||
|
||||
Rudyard Kipling wrote "If" in 1895 and published it in his 1910 collection Rewards and Fairies. This ode to data control was first published in 2022. Here is the full text.
|
||||
|
||||
```poem
|
||||
If you can keep your data when all about you
|
||||
Are losing theirs and blaming it on you;
|
||||
If you can trust your metrics when analysts doubt you,
|
||||
But make allowance for their doubting too:
|
||||
|
||||
If you can query and not be tired by waiting,
|
||||
Or building models, adjust for outliers,
|
||||
Define ETL modules that are self-validating,
|
||||
And require contracts from data suppliers;
|
||||
|
||||
|
||||
If you can report - and not make KPIs your master;
|
||||
If you can forecast - and not make compliance your aim,
|
||||
If you can make the overnight batch faster
|
||||
And ensure the end results are just the same:
|
||||
|
||||
If you can't bear two versions of truth spoken
|
||||
Produced by silos to make a trap for fools,
|
||||
Or watch systems you gave your life to, broken,
|
||||
And stoop and build 'em up with off-the-shelf tools;
|
||||
|
||||
|
||||
If you would make one lake with all your data
|
||||
And risk it on one vendor's big bang plan,
|
||||
And lose, and start again at invitation-to-tender
|
||||
And salvage from the project, what you can:
|
||||
|
||||
If you can force your flat files and mainframe
|
||||
To serve datamarts when key DBAs retire,
|
||||
And so hold on when stakeholders proclaim
|
||||
Why (oh why) is our Data Quality so dire?
|
||||
|
||||
|
||||
If you can talk with architects & keep your virtue,
|
||||
Or walk with CEOs - nor lose the accounting touch,
|
||||
If neither UTF-8 nor PII can hurt you,
|
||||
If all teams count with you, but none too much:
|
||||
|
||||
If you can keep Data Owners beholden
|
||||
To uploads that are timely and accurate and whole,
|
||||
Yours is the Earth and clean records (golden),
|
||||
And - which is more - you'll have Data Control!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*If you can't keep your data when all about you are losing theirs - [Data Controller for SAS](https://datacontroller.io) can help. Capture, review and approval for every change, with a full audit trail, on SAS Viya, SAS 9 EBI and SASjs Server.*
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
If you can keep your data when all about you
|
||||
are losing theirs, and blaming it on you...
|
||||
|
||||
#datagovernance #dataquality #masterdatamanagement
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Classic book title-page cover on a dark slate (#314351) background with a faint vignette and dot grid. A centred cream paper card with a drop shadow, double rule and green spine hint holds the title page: "FIRST PUBLISHED 2022" in small caps, green ornament rules, "IF" in large bold serif, "An Ode to Data Control" in serif, "after Rudyard Kipling" in italic, and the closing couplet "And - which is more - you'll have Data Control!" above a green rule. 1200x627 landscape, composed programmatically with PIL in the Data Controller brand palette rather than generated by an image model.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,155 @@
|
||||
---
|
||||
title: '28 Ways to Be Missing in SAS'
|
||||
description: A SAS numeric missing is not a lone wolf - there are 28 of them, and Data Controller has supported all of them since v4. How they work, and what the validation rules do with them.
|
||||
date: '2026-09-22 09:00:00'
|
||||
author: 'Allan Bowe'
|
||||
authorLink: https://www.linkedin.com/in/allanbowe/
|
||||
tags:
|
||||
- Special Missings
|
||||
- Data Quality
|
||||
previewImg: './cover.jpeg'
|
||||
---
|
||||
|
||||
# 28 Ways to Be Missing in SAS
|
||||
|
||||
A SAS numeric missing is not a lone wolf: the ordinary missing (`.`) is one of **28** distinct missing values a numeric variable can hold. The other 27 are written with a single character - the letters `A` to `Z`, or an underscore (`._`).
|
||||
|
||||
They exist because "missing" is usually not the whole story. A survey question that was never reached, a reading that was illegible, a value the respondent refused to give - in a well-run process those are different facts, and a lone `.` throws the difference away. Special missings record *why* the value is missing.
|
||||
|
||||
## They are numbers, not text
|
||||
|
||||
- Arithmetic on them yields missing - `.A + 1` is `.`
|
||||
- `PROC MEANS`, `PROC SUMMARY` and the other summarisation procedures exclude them, exactly as they exclude `.`
|
||||
- They sort below every non-missing number, in the order `._`, then `.`, then `.A` to `.Z`
|
||||
- `NMISS()` and `CMISS()` count them as missing
|
||||
- Converting one to text drops the period - `CATS(.A)` is the string `A`
|
||||
|
||||
One display detail is worth knowing: a regular missing prints as `.`, unless `OPTIONS MISSING=` changes that character - set it to blank and a regular missing renders as an empty cell. The option affects only the regular missing; `._` and `.A`-`.Z` always print as their own letter. So a blank cell in a SAS listing is still unambiguously a regular missing, and a lone letter is still a special one. Data Controller itself is unaffected either way - it sends a regular missing to the browser as `null` and a special missing as its letter.
|
||||
|
||||
In a SAS dataset they are written with a leading period (`.A`, `.B` ... `._`). In Data Controller you type the letter or the underscore, with or without that period - `.a` and `a` are the same missing - and the letter is not case sensitive. Two letters, or a letter mixed with a number, are refused rather than guessed.
|
||||
|
||||
There is one cell where the letter cannot be typed at all. A numeric column that carries a date, datetime or time format is edited through a date picker rather than the numeric editor, and a picker accepts only a date.
|
||||
|
||||
## Carrying them between the browser and SAS
|
||||
|
||||
The Data Controller frontend and the SAS backend exchange data as JSON, and JSON has no way to express a letter as a numeric value - `A` is a string. The conversion is handled in the open source [SASjs Adapter](https://github.com/sasjs/adapter#variable-types):
|
||||
|
||||
- The adapter infers each column's SAS type from the values it is given. All numeric values mean a numeric column, all strings mean a character column, and a column holding a single character (`a`-`z`, `_` or `.`) alongside numeric values is numeric, with the lone characters written to SAS as special missings.
|
||||
- `null` becomes `.` or an empty string, according to the type derived for that column.
|
||||
- Two cases cannot be inferred from the values alone: a numeric column containing *only* special missings looks like a single character column, and a character column containing only nulls looks numeric. For those, the adapter accepts an explicit format for the column - and Data Controller sends one automatically, because it already knows each column's SAS format from the metadata returned by the backend.
|
||||
- A value that is neither a number nor a single valid character is refused rather than guessed - `aaaa`, or `!` in a numeric column.
|
||||
- A lone `.` is accepted as another way of typing the regular missing.
|
||||
|
||||
There is nothing to configure. Special missings are available by default, for numeric cells - a date, datetime or time formatted column aside, since those edit through a date picker.
|
||||
|
||||
Once in SAS they are ordinary values, so they are what the approval DIFF screen compares, and the DIFF's formatted / unformatted switch shows either the formatted representation or the raw value - useful for confirming exactly which missing was set.
|
||||
|
||||
## What the Data Controller validation rules do with them
|
||||
|
||||
These are Data Controller's own rules, configured per column in the `MPE_VALIDATIONS` table and applied in the browser as you edit and submit.
|
||||
|
||||
- `NOTNULL` - rejects one. A special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it as well. A primary key column is treated as NOT NULL whether or not a rule is configured for it
|
||||
- `HARDREGEX` - checked against the pattern like any other value; unlike blanks and the plain `.`, special missings are **not** exempt, so a numeric column that carries them needs a pattern which allows for a single letter
|
||||
- `SOFTREGEX` - the same check, but a failure is only a warning rather than a block, and it is ignored entirely if the column also has a `HARDREGEX` rule
|
||||
- `SOFTSELECT` / `HARDSELECT` - both support them. The dropdown lists a special missing as a bare letter alongside the ordinary values, and a hard rule then accepts it like any other listed value - it still rejects a value that is not in the list
|
||||
- `ROUND` - no effect. It only rounds values that are numbers, so a special missing is left as it was typed
|
||||
|
||||
The range rules compare in the order SAS itself uses, which is what lets a range be written in special missings. SAS puts every missing below every non-missing value, and orders the missing values among themselves: `._` is the lowest, then the regular missing, then `.A` through `.Z`. A range therefore means exactly what SAS would mean by it:
|
||||
|
||||
- `MINVAL .A` with `MAXVAL .C` accepts `.B` and rejects `.D`, and a blank - the regular missing - fails that floor because it sorts below `.A`
|
||||
- a number sits above every missing, so it passes a floor of `.A` and fails a ceiling of `.C`
|
||||
- against a numeric bound the ordering does the obvious thing: a missing sorts below every number, so it fails a `MINVAL` of 1 and passes a `MAXVAL` of 100. Use `NOTNULL` if the column has to be populated
|
||||
- a rule value that is neither a number nor a special missing - a typo such as `..` or `AB` - satisfies nothing, so the column fails until the rule is corrected
|
||||
|
||||
A formula is the one case where a special missing genuinely does not work:
|
||||
|
||||
- `HARDFORMULA` / `SOFTFORMULA` - a formula that reads a special-missing cell does not compute; the grid's spreadsheet engine returns `#VALUE!` for that row, where the plain `.` contributes 0
|
||||
|
||||
`CASE` (`UPCASE` / `LOWCASE`) is a character rule, so it does not apply: SAS hands the browser a special missing as an uppercase letter, so there is no case to enforce, and a `CASE` rule on a numeric column would reject the column's numbers rather than the missing.
|
||||
|
||||
Worth knowing about the regex rules: although the pattern is written in SAS PRX syntax, the check itself runs in the browser - SAS only parses the pattern (`PRXPARSE`) when the rule is saved - so the two engines can disagree on exotic patterns, and SAS pads a numeric-to-character conversion, so a pattern re-used in SAS needs `strip()` for an anchored match.
|
||||
|
||||
In short, a special missing counts as a value for the pattern and dropdown rules, and takes its own place in the order for the range rules: it sits below every number, so a numeric minimum rejects it and a numeric maximum accepts it, while a range written in special missings is decided among the missing values themselves.
|
||||
|
||||
## See it in action
|
||||
|
||||
The recording below runs the whole cycle on one table: entering special missings, the rules that reject them, submitting the changes, approving them, and reviewing the DIFF - including a change from one special missing to another, and the formatted / unformatted switch on a date column.
|
||||
|
||||
It also shows the range rules doing what the section above describes. A special missing is refused by a numeric `MINVAL` and accepted by a numeric `MAXVAL`, and on a column carrying `MINVAL .A` with `MAXVAL .C`, `.B` is taken while `.D` is refused.
|
||||
|
||||
<div style="position: relative; padding-top: 56.25%; margin-bottom: 2rem;"><iframe title="Special Missings in Data Controller" width="100%" height="100%" src="https://vid.4gl.io/videos/embed/9UQZzCNBU3zPNQYxdzyV3A?peertubeLink=0" style="border: 0px; position: absolute; inset: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe></div>
|
||||
|
||||
<!--
|
||||
LinkedIn version of this post - publish it with the "Special Missings in Data
|
||||
Controller" video attached. Kept in sync with the copy above: same points, same
|
||||
claims, same order.
|
||||
|
||||
A SAS numeric missing is not a lone wolf. There are 28 of them.
|
||||
|
||||
The ordinary missing (.) is the best known of the 28. The other 27 are single characters - the letters A to Z, or an underscore (._) - and they let you record WHY a value is missing: the question was never reached, the reading was illegible, the respondent refused.
|
||||
|
||||
They are real numeric values, not text:
|
||||
|
||||
- .A + 1 is .
|
||||
- PROC MEANS excludes them, like any other missing
|
||||
- they sort below every number: ._ then . then .A to .Z
|
||||
- NMISS() and CMISS() count them as missing
|
||||
- a regular missing prints as . unless OPTIONS MISSING= changes it - and that option never touches a special missing
|
||||
|
||||
The hard part is every tool that is not SAS. JSON has no way to say "this number is a letter" - A is just a string. So a value that is perfectly legal in a SAS dataset quietly breaks in the browser, in Excel, in an API.
|
||||
|
||||
We solved that in the open source SASjs Adapter, and it has been in Data Controller for SAS since v4:
|
||||
|
||||
- the adapter infers the column type from the values, so a column of numbers that also holds a lone letter is written to SAS as numeric, with the letter as a special missing
|
||||
- where the type cannot be inferred - a numeric column holding ONLY special missings - Data Controller passes the column format explicitly
|
||||
- you type the letter or the underscore, with or without the period (a, .a, _, ._), and case does not matter
|
||||
- one exception: a date, datetime or time formatted numeric column edits through a date picker, which takes only a date
|
||||
|
||||
How they behave in Data Controller's validation rules:
|
||||
|
||||
- NOTNULL rejects one: a special missing is a missing value, so it fails the rule, and a physical NOT NULL constraint on the target table rejects it too. A primary key column is NOT NULL whether or not a rule is configured
|
||||
- HARDREGEX checks it against the pattern, unlike blanks and plain .
|
||||
- SOFTREGEX warns instead of blocking (and is ignored if the column also has a HARDREGEX)
|
||||
- SOFTSELECT and HARDSELECT both support them - the dropdown lists the missing as a bare letter, and a hard rule accepts it like any other listed value
|
||||
- ROUND leaves it alone, because it only rounds numbers
|
||||
- a HARDFORMULA/SOFTFORMULA that reads a special-missing cell returns #VALUE! rather than a number
|
||||
|
||||
The range rules compare in SAS's own order, so a range can be written in special missings: MINVAL .A with MAXVAL .C accepts .B and rejects .D. A missing sorts below every number, so it fails a numeric MINVAL and passes a numeric MAXVAL; a number sits above every missing.
|
||||
|
||||
In short: a value for the pattern and dropdown rules, and its own place in the order for the range rules - below every number, so a numeric minimum rejects it and a numeric maximum accepts it.
|
||||
|
||||
The video shows the whole cycle - entering them, the rejections, submit, approve, and the DIFF, including a range written in special missings.
|
||||
|
||||
#sas #datacapture #mdm #dataquality
|
||||
|
||||
video: https://vid.4gl.io/w/9UQZzCNBU3zPNQYxdzyV3A
|
||||
|
||||
Image prompt: Cinematic editorial cover illustration: a large pack of wolves, a
|
||||
dozen or more, spread wide and moving together across a vast snow plain at dusk,
|
||||
seen from a low wide angle. One wolf stands apart from the group on the left of
|
||||
frame, turned back toward the pack. The pack is rendered in cool blue-grey and
|
||||
silver; the lone wolf catches the only warm light in the scene, a single low
|
||||
amber sun. Overcast dusk sky, faint falling snow, long soft shadows, generous
|
||||
empty sky and snow to the upper third so the wide crop breathes. Painterly
|
||||
digital illustration, muted desaturated palette, soft depth of field with the
|
||||
distant wolves falling out of focus, no text, no letters, no numbers, no logos,
|
||||
no watermark.
|
||||
|
||||
Constraints: no text, no letters, no numbers, no digits, no symbols, no
|
||||
captions, no logos, no watermark, no signature, no border, no frame, no collar,
|
||||
no harness, no humans, no buildings, and do not ask for an exact head count of
|
||||
28 - a crowded pack reads worse than a dozen clear animals, and the number
|
||||
belongs in the headline, not the artwork. The no-text rule matters more than
|
||||
usual here: the subject is letters standing in for numbers, so a stray glyph
|
||||
anywhere undercuts the cover.
|
||||
|
||||
Output: save as ./cover.jpeg at 1.91:1 (1200x627, matching the other feed
|
||||
covers and doubling as the LinkedIn share card), and set
|
||||
previewImg: './cover.jpeg' in the front matter above. Do not also embed the
|
||||
image in the markdown body. Full prompt, variants and rationale:
|
||||
https://paste.4gl.io/?513711f5bde3610e#9LZCA9xzQHDy3CycXmsWU5AoVNtjBZmPotUHHtHTAerF
|
||||
|
||||
Fallback variant if the pack composition comes back muddled: a single wolf
|
||||
standing alone on a snow plain at dusk, its shadow stretching toward a distant
|
||||
pack reduced to small silhouettes on the horizon.
|
||||
-->
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
title: 'Support Diagnostics: browser_info and browser_url_vars'
|
||||
description: Data Controller sends two input tables with the startup service and with every service that runs customer code - the browser context and the page URL parameters. A support ticket can be answered from the job log, and a hook can read them.
|
||||
date: '2026-09-27 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './support-diagnostics.jpeg'
|
||||
---
|
||||
|
||||
# Support Diagnostics: browser_info and browser_url_vars
|
||||
|
||||
A support ticket usually starts with a round of questions. Which browser? Which timezone? Which build? What was in the URL? Data Controller now answers them itself. The frontend sends two input tables - `browser_info` and `browser_url_vars` - with the startup service and with every service that runs customer-provided code, so the context of a session is in the job log before anyone asks for it, and a hook script can read it.
|
||||
|
||||
This ships with the next Data Controller release.
|
||||
|
||||
## The two tables
|
||||
|
||||
Both are ordinary input tables on the request: the adapter turns each array in the payload into a table in the service's WORK library, so they arrive as `work.browser_info` and `work.browser_url_vars`.
|
||||
|
||||
### browser_info
|
||||
|
||||
A single row, describing the client that made the request:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `url` | The URL of the Data Controller page itself (the iframe), not the document embedding it - so an embedded editor is distinguishable from a standalone one. |
|
||||
| `referrer` | The embedding document, from `document.referrer`. For an embedded report this is the report URL. |
|
||||
| `timezone` | The browser's IANA timezone, e.g. `Europe/Berlin`. |
|
||||
| `tz_offset` | The browser's UTC offset in minutes, as `Date.getTimezoneOffset()` returns it - positive west of UTC, so `Europe/Berlin` in summer is `-120`. |
|
||||
| `locale` | The browser locale, e.g. `en-GB`. |
|
||||
| `dc_version` | The Data Controller build, as `appinfo()` reports it. |
|
||||
| `adapter_version` | The `@sasjs/adapter` version the client was built with. |
|
||||
| `browser` | The browser family, parsed from the user agent. |
|
||||
| `browser_version` | The browser version. |
|
||||
| `platform` | The operating system. |
|
||||
| `user_agent` | The raw `navigator.userAgent` string. |
|
||||
|
||||
### browser_url_vars
|
||||
|
||||
One row per URL parameter:
|
||||
|
||||
| Column | What it holds |
|
||||
|---|---|
|
||||
| `name` | The parameter name. |
|
||||
| `value` | The parameter value. |
|
||||
|
||||
Parameters are read from both the search string and the hash query string, because Angular routes carry them after the `#`. Where the same name appears in both, the search string wins. The table is sent only when the page URL has at least one parameter.
|
||||
|
||||
## Which services receive them
|
||||
|
||||
| Service | When |
|
||||
|---|---|
|
||||
| `public/startupservice` | Once per session, so every job log carries the session context |
|
||||
| `editors/getdata` | The PRE_EDIT_HOOK |
|
||||
| `editors/stagedata` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/restore` | The POST_EDIT_HOOK, through the loader |
|
||||
| `editors/getdynamiccolvals` | The dynamic cell dropdown programs |
|
||||
| `auditors/postdata` | The PRE_APPROVE_HOOK and POST_APPROVE_HOOK |
|
||||
| `editors/loadfile` | The POST_EDIT_HOOK - see the caveat below |
|
||||
|
||||
Those are the services that run customer code. The viewer's `viewlibs`, `viewtables` and `viewdata`, the metadata services and `usernav` receive neither table, so the high-frequency calls stay lean.
|
||||
|
||||
## Reading them in a hook
|
||||
|
||||
A hook script is `%include`d into the running service, so it shares the service's WORK library and can read the tables directly. Take an embedded editor opened at:
|
||||
|
||||
```
|
||||
https://datacontroller.io/#/editor/SASHELP.CLASS?labels=true&embed=va
|
||||
```
|
||||
|
||||
`browser_url_vars` then holds a row per parameter - `labels=true` and `embed=va` - and a hook can read either by name:
|
||||
|
||||
```sas
|
||||
%let labels=N;
|
||||
%let embed=N;
|
||||
|
||||
%if %sysfunc(exist(work.browser_url_vars)) %then %do;
|
||||
proc sql noprint;
|
||||
select value into :labels
|
||||
from work.browser_url_vars
|
||||
where name = 'labels';
|
||||
select value into :embed
|
||||
from work.browser_url_vars
|
||||
where name = 'embed';
|
||||
quit;
|
||||
%end;
|
||||
```
|
||||
|
||||
Or branch on what the user's browser reports:
|
||||
|
||||
```sas
|
||||
data _null_;
|
||||
set work.browser_info;
|
||||
call symputx('dc_timezone', timezone);
|
||||
call symputx('dc_locale', locale);
|
||||
run;
|
||||
```
|
||||
|
||||
Note the `exist()` guard. A service called directly, or by a frontend older than this feature, has no `work.browser_info` at all, so an unguarded `set` or `select` would fail the job. Guard first, then read.
|
||||
|
||||
## In the job log
|
||||
|
||||
Turn debug on and the session initialisation writes both tables to the log, so a ticket can be diagnosed without a second round of questions:
|
||||
|
||||
```
|
||||
NOTE: MPEINIT: work.browser_url_vars:
|
||||
name=embed value=va
|
||||
NOTE: MPEINIT: work.browser_info:
|
||||
url=... referrer=... timezone=Europe/Berlin tz_offset=-120 locale=en-GB
|
||||
dc_version=... adapter_version=... browser=Chrome browser_version=...
|
||||
platform=Linux user_agent=...
|
||||
```
|
||||
|
||||
The debug value depends on the path - `&_debug=131` on the Compute API and SAS 9, `&_debug=128` on the Viya web (JES) path with `runAsTask` enabled, which is what the frontend sends there. Either way, switching debug on in the app is enough.
|
||||
|
||||
## Worth knowing
|
||||
|
||||
- The values are supplied by the client. Treat them as diagnostics hints, never as a security boundary.
|
||||
- `editors/loadfile` is reached through the adapter's multipart file upload, which carries no input tables, so in normal use that service - and the hook it runs - does not receive them. It is on the list for completeness.
|
||||
- Nothing else pays for the tables: they travel only with the services above.
|
||||
|
||||
Full detail, including the column definitions and the log output, is in the [troubleshooting documentation](https://docs.datacontroller.io/dci-troubleshooting/).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
A support ticket starts with questions. Which browser? Which timezone? Which build? What was in the URL?
|
||||
|
||||
Data Controller now answers them itself.
|
||||
|
||||
Two tables travel with the startup service, and with every service that runs your own code:
|
||||
|
||||
→ browser_info - one row: browser and version, platform, timezone and UTC offset, locale, the DC and adapter builds, the page URL and the referrer
|
||||
|
||||
→ browser_url_vars - one row per URL parameter, name and value, read straight into a macro variable
|
||||
|
||||
Both land in the job log when debug is on, so a ticket can be diagnosed from the log instead of a round trip. Both are readable from a hook script - %include shares the service's WORK library, so a hook can branch on the user's timezone, or pick up a parameter from the URL.
|
||||
|
||||
They are client supplied values: diagnostics hints, not a security boundary. Guard with %sysfunc(exist()) - a service called directly, or an older frontend, sends nothing.
|
||||
|
||||
✅ Full detail in the Data Controller documentation - link in the comments.
|
||||
|
||||
#sas #sasviya #datagovernance #datacapture #sasjs
|
||||
-->
|
||||
|
||||
<!-- Image prompt:
|
||||
Generate a 16:9 landscape illustration for a B2B data-governance blog cover, 1200x627.
|
||||
|
||||
Style: flat vector illustration, dark slate background (#314351) with a faint evenly spaced dot grid, brand green accent (#90c445), lighter slate panels (#2e4252), soft diffused shadows, subtle depth.
|
||||
|
||||
Scene: two rounded panels side by side, each with a header bar in brand green and four rows of rounded cells. The left panel carries short uniform value blocks (the browser context); the right panel carries name-and-value pairs, drawn as a narrow block beside a wider one (the URL parameters), with a single row in the left panel highlighted in green. Between and below the panels a thin vertical connector line runs down to a wide, flat log strip of alternating short green rules on a lighter slate panel, suggesting the tables being written to a job log.
|
||||
|
||||
Minimal text, no letters, no numbers, no logos, no watermark. Keep the subject inside the central square so a 1:1 crop is safe; the outer left and right thirds may be croppable background only.
|
||||
|
||||
Output: save as ./support-diagnostics.jpeg at 1200x627 (matching the other feed covers, and doubling as the LinkedIn share card). Do not also embed the image in the markdown body.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 'Data Controller for SAS® - Who Is It For?'
|
||||
description: Five roles, five questions - what Data Controller for SAS® answers for administrators, auditors, managers, data owners and purchasers.
|
||||
date: '2026-09-26 09:00:00'
|
||||
author: 'Data Controller'
|
||||
authorLink: https://www.linkedin.com/showcase/data-controller-for-sas
|
||||
tags:
|
||||
- Announcements
|
||||
previewImg: './who-is-data-controller-for.jpeg'
|
||||
---
|
||||
|
||||
# Data Controller for SAS® - Who Is It For?
|
||||
|
||||
Data Controller for SAS® gives business users a safe way to add, modify and delete data in SAS tables and databases, without handing them write access to production. That one capability answers a different question for each role that touches the data - so here is the short version, role by role.
|
||||
|
||||
## Data Administrators
|
||||
|
||||
Would you grant self-serve data updates in SAS if there was no risk to the data model? That is the trade Data Controller offers. End users never get direct write access to the target tables - every change is staged, validated at the point of entry, and applied by the service itself. Data Controller changes data, not structure, so your data model stays as it is.
|
||||
|
||||
## Data Auditors
|
||||
|
||||
Do you need to know who made a particular data change, what that change was, when it was made, and who signed it off? Every submission carries a full audit trail, and any associated Excel file is retained alongside it, so the source of a change can be reviewed long after it was loaded.
|
||||
|
||||
## Data Managers
|
||||
|
||||
Do you want to load, modify or delete production data without writing repetitive code, saving sensitive data to shared filesystems, building and deploying jobs, or crafting long-winded change requests? Adding a new editable table is configuration rather than development - the grid is generated from the target table's metadata, and the load type (composite or retained keys, SCD2, bitemporal) is picked from a list.
|
||||
|
||||
## Data Owners
|
||||
|
||||
Do you like to review data changes before they are applied? Approvers see the diff - new, changed and deleted rows - and nothing lands until it is signed off. Email alerts can tell you when data is changed or about to change, and a completed load can trigger a SAS job downstream. If the wrong change gets approved anyway, the table can be rolled back to a previous state - and the rollback is itself a new, reviewable change, so the audit trail keeps both the mistake and the correction.
|
||||
|
||||
## Purchasers
|
||||
|
||||
How about a business friendly, no lock-in software licence? The price list is public, every package includes unlimited users, and a perpetual licence is available. The Community package is free, so evaluating Data Controller costs nothing but the installation.
|
||||
|
||||
## What it is, in one line
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism. Configuration is quick, installation is easy, and everything runs on your own SAS platform with no external calls.
|
||||
|
||||
Multiple load types are supported (composite and retained keys, SCD2, bitemporal, format catalogs), data can be validated at the point of entry, and Excel end-user computing files can be dragged straight into SAS. Data Rollback, Data Catalog and Data Viewer come as standard.
|
||||
|
||||
Data Control.
|
||||
|
||||
Want to see it working on your own data? [Book a Discovery Call](/contact).
|
||||
|
||||
<!--
|
||||
Source LinkedIn post:
|
||||
|
||||
Data Administrators → would you grant self-serve data updates in SAS, if there was no risk to the data model?
|
||||
|
||||
Data Auditors → do you require oversight of who made a particular data change, what that change was, when it was made, and who signed it off? And to review the original excel, if used as a source?
|
||||
|
||||
Data Managers → do you prefer to load / modify / delete production data WITHOUT writing repetitive code, saving sensitive data to shared filesystems, building & deploying jobs, or crafting long-winded change requests?
|
||||
|
||||
Data Owners → do you like to review data changes before they are applied? How about receiving email alerts or the ability to set up job triggers after tables are updated?
|
||||
|
||||
Purchasers → how about a business friendly, no lock-in software licence?
|
||||
|
||||
Data Controller for SAS® is a robust solution for secure data uploads into any SAS-connected database, with full audit trail and approval mechanism.
|
||||
|
||||
Installation is easy and configuration is quick ⚡ Multiple load types supported (composite / retained keys, SCD2, Bitemporal, Format catalogs). Perform data validation at point of entry. Drag & drop Excel EUCs straight into SAS.
|
||||
|
||||
Data Rollback, Data Catalog, Data Viewer.
|
||||
|
||||
Data Control. ✅
|
||||
|
||||
#sas #sasviya #sasusers #datagovernance
|
||||
|
||||
https://lnkd.in/dunqWhnt
|
||||
|
||||
Image prompt:
|
||||
|
||||
Flat vector illustration, dark slate blue background (#314351), wide 1.91:1 banner (1200 x 627). Five simplified human figures stand in a shallow arc across the centre, each rendered in flat corporate style with minimal facial detail: an administrator beside a padlock, an auditor with a magnifying glass over a document, a manager holding a spreadsheet with an upward arrow, a data owner holding an approval tick, and a purchaser holding a price tag. Behind them, a translucent dashboard panel shows a data grid with one row highlighted as a pending change, and a small diff of new, changed and deleted rows. Thin connecting lines link each figure to the panel, suggesting that every role sees the same controlled workflow. Bright green accent (#90c445) used sparingly on the approval tick, the highlighted row and the connecting lines. Subtle depth with soft shadows, crisp edges. No text, no words, no letters, no logos, no watermarks. Keep the figures and the panel inside the central square so the image is safe to crop to 1:1 - the outer left and right thirds are background only.
|
||||
-->
|
||||
|
After Width: | Height: | Size: 138 KiB |
@@ -55,7 +55,6 @@
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"bootstrap": "^5.0.0-beta3",
|
||||
"cloudron-surfer": "^6.4.1",
|
||||
"jquery": "^3.6.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
{
|
||||
"$comment": "Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline",
|
||||
"owner": "Site maintainers (allan@4gl.io)",
|
||||
"reason": "Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.",
|
||||
"accepted": [
|
||||
{
|
||||
"pkg": "@ardatan/relay-compiler",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@babel/plugin-transform-modules-systemjs",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fv7c-fp4j-7gwp",
|
||||
"title": "@babel/plugin-transform-modules-systemjs generates arbitrary code when compiling malicious input",
|
||||
"range": ">=7.12.0 <=7.29.3"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/add",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/core",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/plugin-helpers",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/schema-ast",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/typescript-operations",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/typescript",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-codegen/visitor-plugin-common",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/code-file-loader",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/graphql-tag-pluck",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/load",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/merge",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/relay-operation-optimizer",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/schema",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "@graphql-tools/utils",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7mx3-vvmw-hjmv",
|
||||
"title": "GraphQL Tools has prototype pollution in well-established utility function `mergeDeep`",
|
||||
"range": "<=12.0.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-35jp-ww65-95wh",
|
||||
"title": "axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `config.proxy`",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3g43-6gmg-66jw",
|
||||
"title": "axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollution Gadget in Config Merge",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3p68-rc4w-qgx5",
|
||||
"title": "Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF",
|
||||
"range": ">=1.0.0 <1.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3pq3-5fj3-cg6v",
|
||||
"title": "Axios: HTTP/2 adapter bypasses configured DNS lookup and proxy controls",
|
||||
"range": ">=1.13.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3w6x-2g7m-8v23",
|
||||
"title": "Axios: Invisible JSON Response Tampering via Prototype Pollution Gadget in `parseReviver`",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-42h9-826w-cgv3",
|
||||
"title": "Axios: Excessive recursion in formDataToJSON can cause denial of service",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-43fc-jf86-j433",
|
||||
"title": "Axios is Vulnerable to Denial of Service via __proto__ Key in mergeConfig",
|
||||
"range": ">=1.0.0 <=1.13.4"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-445q-vr5w-6q77",
|
||||
"title": "Axios: CRLF Injection in multipart/form-data body via unsanitized blob.type in formDataToStream",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-4hqw-qxg8-jxx2",
|
||||
"title": "Axios: Fetch Adapter Header Injection via Inherited FormData getHeaders",
|
||||
"range": ">=1.12.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-542g-h47m-68v8",
|
||||
"title": "Axios: Denial of Service via Unhandled 'error' Event in HTTP/2 ClientHttp2Session Initialization",
|
||||
"range": ">=1.13.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5c9x-8gcm-mpgx",
|
||||
"title": "Axios' HTTP adapter-streamed uploads bypass maxBodyLength when maxRedirects: 0",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-62hf-57xw-28j9",
|
||||
"title": "Axios: unbounded recursion in toFormData causes DoS via deeply nested request data",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6chq-wfr3-2hj9",
|
||||
"title": "Axios: Header Injection via Prototype Pollution",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-777c-7fjr-54vf",
|
||||
"title": "Allocation of Resources Without Limits or Throttling in Axios",
|
||||
"range": ">=1.7.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7q8q-rj6j-mhjq",
|
||||
"title": "Axios: Nested axios option objects can consume polluted prototype values",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-898c-q2cr-xwhg",
|
||||
"title": "axios has DoS & Header Injection via Prototype Pollution Read-Side Gadgets in axios merge functions",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-9fr6-4gfg-395g",
|
||||
"title": "Axios: Prototype-Pollution Gadget in the Default Instance Allows Inherited Object.prototype.method to Override HTTP Method",
|
||||
"range": ">=1.0.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fvcv-3m26-pcqx",
|
||||
"title": "Axios has Unrestricted Cloud Metadata Exfiltration via Header Injection Chain",
|
||||
"range": ">=1.0.0 <1.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-hfxv-24rg-xrqf",
|
||||
"title": "Axios: Regular Expression Denial of Service (ReDoS) via Cookie Name Injection",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-j5f8-grm9-p9fc",
|
||||
"title": "Axios: Proxy-Authorization header leaks to redirect target when proxy is re-evaluated to direct connection",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-j8rh-479h-cp32",
|
||||
"title": "Axios: Header Injection via Inherited headers After Minimal Interceptor",
|
||||
"range": ">=1.0.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-jqh4-m9w3-8hp9",
|
||||
"title": "Axios: Fetch adapter `ReadableStream` uploads bypass `maxBodyLength`",
|
||||
"range": ">=1.7.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-m7pr-hjqh-92cm",
|
||||
"title": "Axios: no_proxy bypass via IP alias allows SSRF",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mmx7-hfxf-jppx",
|
||||
"title": "Axios: Prototype pollution gadgets can alter axios request construction",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mwf2-3pr3-8698",
|
||||
"title": "Axios: HTTP/2 streamed uploads bypass `maxBodyLength`",
|
||||
"range": ">=1.13.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-p92q-9vqr-4j8v",
|
||||
"title": "Axios: Proxy-Authorization Credential Leak to Origin Server Across HTTP-to-HTTPS Redirect in Axios Node.js HTTP Adapter",
|
||||
"range": ">=1.0.0 <1.16.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pf86-5x62-jrwf",
|
||||
"title": "Axios: Prototype Pollution Gadgets - Response Tampering, Data Exfiltration, and Request Hijacking",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pmv8-rq9r-6j72",
|
||||
"title": "Axios: Deep formToJSON Key Recursion Can Cause Denial of Service",
|
||||
"range": ">=1.0.0 <1.18.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-pmwg-cvhr-8vh7",
|
||||
"title": "Axios: Incomplete Fix for CVE-2025-62718 — NO_PROXY Protection Bypassed via RFC 1122 Loopback Subnet (127.0.0.0/8) in Axios 1.15.0",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q8qp-cvcw-x6jj",
|
||||
"title": "Axios has prototype pollution read-side gadgets in HTTP adapter that allow credential injection and request hijacking",
|
||||
"range": ">=1.0.0 <1.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vf2m-468p-8v99",
|
||||
"title": "Axios: HTTP adapter streamed responses bypass maxContentLength",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vh66-26gq-q6x8",
|
||||
"title": "Axios: Prototype pollution gadget in fetch adapter can alter outbound requests",
|
||||
"range": ">=1.7.0 <1.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-w9j2-pvgh-6h63",
|
||||
"title": "Axios: Authentication Bypass via Prototype Pollution Gadget in `validateStatus` Merge Strategy",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xhjh-pmcv-23jw",
|
||||
"title": "Axios: Null Byte Injection via Reverse-Encoding in AxiosURLSearchParams",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "axios",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xx6v-rp6x-q39c",
|
||||
"title": "Axios: XSRF Token Cross-Origin Leakage via Prototype Pollution Gadget in `withXSRFToken` Boolean Coercion",
|
||||
"range": ">=1.0.0 <1.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-27r3-jxmh-p828",
|
||||
"title": "body-parser vulnerable to Denial of Service via trailing bytes after a compressed request body",
|
||||
"range": "<1.20.9"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qwcr-r2fm-qrc7",
|
||||
"title": "body-parser vulnerable to denial of service when url encoding is enabled",
|
||||
"range": "<1.20.3"
|
||||
},
|
||||
{
|
||||
"pkg": "body-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v422-hmwv-36x6",
|
||||
"title": "body-parser vulnerable to denial of service when invalid limit value silently disables size enforcement",
|
||||
"range": "<1.20.6"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3jxr-9vmj-r5cp",
|
||||
"title": "brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups",
|
||||
"range": "<1.1.16"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6j4f-fj2g-mc7p",
|
||||
"title": "brace-expansion: DoS via uncontrolled recursion in parseCommaParts causing stack exhaustion",
|
||||
"range": "<1.1.19"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f886-m6hf-6m8v",
|
||||
"title": "brace-expansion: Zero-step sequence causes process hang and memory exhaustion",
|
||||
"range": "<1.1.13"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mh99-v99m-4gvg",
|
||||
"title": "brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash",
|
||||
"range": "<1.1.17"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q2hr-2g5m-vwhr",
|
||||
"title": "brace-expansion: Quadratic-time expansion of the `{a},b}` rewrite causes CPU denial of service",
|
||||
"range": "<1.1.21"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qhr7-859c-m2p7",
|
||||
"title": "brace-expansion: DoS via uncontrolled recursion on nested brace groups causing stack exhaustion",
|
||||
"range": "<1.1.20"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rgw5-rvv9-x895",
|
||||
"title": "brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation",
|
||||
"range": "<1.1.18"
|
||||
},
|
||||
{
|
||||
"pkg": "brace-expansion",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v6h2-p8h4-qcjw",
|
||||
"title": "brace-expansion Regular Expression Denial of Service vulnerability",
|
||||
"range": ">=1.0.0 <=1.1.11"
|
||||
},
|
||||
{
|
||||
"pkg": "browserslist",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-73wf-gq98-2v4g",
|
||||
"title": "Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)",
|
||||
"range": "<=4.28.6"
|
||||
},
|
||||
{
|
||||
"pkg": "browserslist",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-c83g-rgw3-j3cx",
|
||||
"title": "Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM",
|
||||
"range": "<=4.28.6"
|
||||
},
|
||||
{
|
||||
"pkg": "compression",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-vc2v-76pw-4v95",
|
||||
"title": "compression vulnerable to Denial of Service via memory leak on premature response close",
|
||||
"range": "<1.8.2"
|
||||
},
|
||||
{
|
||||
"pkg": "cross-spawn",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3xgq-45jj-v275",
|
||||
"title": "Regular Expression Denial of Service (ReDoS) in cross-spawn",
|
||||
"range": ">=7.0.0 <7.0.5"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2gc4-cqfq-p2gv",
|
||||
"title": "Socket.IO: Engine.IO Protocol Revision Mismatch DoS",
|
||||
"range": ">=6.6.0 <6.6.10"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gr94-w7qr-f4j3",
|
||||
"title": "Socket.IO: Engine.IO WebTransport SID DoS",
|
||||
"range": ">=6.5.0 <6.6.7"
|
||||
},
|
||||
{
|
||||
"pkg": "engine.io",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r635-g3xr-vw7x",
|
||||
"title": "Socket.IO: Engine.IO Polling Transport Connection Exhaustion",
|
||||
"range": ">=4.1.0 <6.6.7"
|
||||
},
|
||||
{
|
||||
"pkg": "express",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qw6h-vgh9-j6wx",
|
||||
"title": "express vulnerable to XSS via response.redirect()",
|
||||
"range": "<4.20.0"
|
||||
},
|
||||
{
|
||||
"pkg": "flatted",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-25h7-pfq9-p65f",
|
||||
"title": "flatted vulnerable to unbounded recursion DoS in parse() revive phase",
|
||||
"range": "<3.4.0"
|
||||
},
|
||||
{
|
||||
"pkg": "flatted",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rf6f-7fwh-wjgh",
|
||||
"title": "Prototype Pollution via parse() in NodeJS flatted",
|
||||
"range": "<=3.4.1"
|
||||
},
|
||||
{
|
||||
"pkg": "form-data",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-hmw2-7cc7-3qxx",
|
||||
"title": "form-data: CRLF injection in form-data via unescaped multipart field names and filenames",
|
||||
"range": ">=4.0.0 <4.0.6"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-manifest",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-plugin-utils",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby-transformer-sharp",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "gatsby",
|
||||
"severity": "high",
|
||||
"advisory": "transitive"
|
||||
},
|
||||
{
|
||||
"pkg": "http-cache-semantics",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-ch52-4w7c-c8xp",
|
||||
"title": "http-cache-semantics max-stale handling can disclose cross-user cached responses",
|
||||
"range": "<=4.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v56q-mh7h-f735",
|
||||
"title": "Immutable.js `List` 32-bit trie overflow → unrecoverable DoS",
|
||||
"range": "<3.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wf6x-7x77-mvgw",
|
||||
"title": "Immutable is vulnerable to Prototype Pollution",
|
||||
"range": "<3.8.3"
|
||||
},
|
||||
{
|
||||
"pkg": "immutable",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xvcm-6775-5m9r",
|
||||
"title": "Immutable: Hash-collision algorithmic complexity denial of service in Immutable.Map/Set",
|
||||
"range": "<3.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6h2x-m376-mqjq",
|
||||
"title": "joi: Quadratic regular-expression backtracking in `Joi.string().isoDate()`",
|
||||
"range": ">=17.2.0 <17.13.7"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6w3j-5fw6-r9vr",
|
||||
"title": "joi: Prototype pollution via a `__proto__` language key in custom messages",
|
||||
"range": ">=17.2.0 <17.13.6"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gg4h-3hg2-grpc",
|
||||
"title": "joi: object().rename() with a template target can set the validated object's prototype",
|
||||
"range": ">=16.0.0 <17.13.5"
|
||||
},
|
||||
{
|
||||
"pkg": "joi",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wr44-6hxh-3jwq",
|
||||
"title": "joi messages compilation allows prototype replacement through __proto__ error codes",
|
||||
"range": "<17.13.8"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2883-xcg3-v3hh",
|
||||
"title": "js-yaml: maxTotalMergeKeys does not limit CPU use for empty merge sources",
|
||||
"range": ">=3.0.0 <3.15.2"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-52cp-r559-cp3m",
|
||||
"title": "js-yaml: YAML merge-key chains can force quadratic CPU consumption",
|
||||
"range": ">=3.0.0 <3.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5p4m-2wfm-xmqj",
|
||||
"title": "JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) — CVE-2026-59870 fix not backported",
|
||||
"range": ">=3.0.0 <3.15.1"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-h67p-54hq-rp68",
|
||||
"title": "JS-YAML: Quadratic-complexity DoS in merge key handling via repeated aliases",
|
||||
"range": "<3.15.0"
|
||||
},
|
||||
{
|
||||
"pkg": "js-yaml",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mh29-5h37-fv8m",
|
||||
"title": "js-yaml has prototype pollution in merge (<<)",
|
||||
"range": "<3.14.2"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f23m-r3pf-42rh",
|
||||
"title": "lodash vulnerable to Prototype Pollution via array path bypass in `_.unset` and `_.omit`",
|
||||
"range": "<=4.17.23"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r5fr-rjxr-66jc",
|
||||
"title": "lodash vulnerable to Code Injection via `_.template` imports key names",
|
||||
"range": ">=4.0.0 <=4.17.23"
|
||||
},
|
||||
{
|
||||
"pkg": "lodash",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xxjr-mmjv-4gpg",
|
||||
"title": "Lodash has Prototype Pollution Vulnerability in `_.unset` and `_.omit` functions",
|
||||
"range": ">=4.0.0 <=4.17.22"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-23c5-xmqv-rm74",
|
||||
"title": "minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions",
|
||||
"range": "<3.1.4"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3ppc-4f35-3m26",
|
||||
"title": "minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern",
|
||||
"range": "<3.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "minimatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-7r86-cg39-jmmj",
|
||||
"title": "minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments",
|
||||
"range": "<3.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3p4h-7m6x-2hcm",
|
||||
"title": "Multer vulnerable to Denial of Service via incomplete cleanup of aborted uploads",
|
||||
"range": ">=2.0.0-alpha.1 <2.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-535w-7cp7-47q4",
|
||||
"title": "multer vulnerable to Denial of Service via oversized array index in field names",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5528-5vmv-3xc2",
|
||||
"title": "Multer Vulnerable to Denial of Service via Uncontrolled Recursion",
|
||||
"range": "<2.1.1"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-72gw-mp4g-v24j",
|
||||
"title": "Multer vulnerable to Denial of Service via deeply nested field names",
|
||||
"range": ">=1.0.0 <2.2.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-q7qp-rjg7-q26m",
|
||||
"title": "multer vulnerable to Denial of Service via a pending asynchronous fileFilter after request teardown",
|
||||
"range": ">=2.0.0 <2.5.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qvfw-j98x-7q72",
|
||||
"title": "multer vulnerable to file size limit bypass via async fileFilter race condition",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-v52c-386h-88mc",
|
||||
"title": "Multer vulnerable to Denial of Service via resource exhaustion",
|
||||
"range": "<2.1.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wc9g-mqfw-jrwm",
|
||||
"title": "multer vulnerable to Denial of Service via crafted multipart field names",
|
||||
"range": "<2.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "multer",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xf7r-hgr6-v32p",
|
||||
"title": "Multer vulnerable to Denial of Service via incomplete cleanup",
|
||||
"range": "<2.1.0"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-28wg-ghj8-5hjv",
|
||||
"title": "nanoid: non-secure generators can loop indefinitely with negative size",
|
||||
"range": "<3.3.16"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2v37-7h3g-55p8",
|
||||
"title": "nanoid: custom generators can loop indefinitely when size is zero",
|
||||
"range": "<3.3.18"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-mwcw-c2x4-8c55",
|
||||
"title": "Predictable results in nanoid generation when given non-integer values",
|
||||
"range": "<3.3.8"
|
||||
},
|
||||
{
|
||||
"pkg": "nanoid",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xwg4-73v4-xw9w",
|
||||
"title": "nanoid: Integer Overflow or Wraparound",
|
||||
"range": "<3.3.12"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-37ch-88jc-xwx2",
|
||||
"title": "path-to-regexp vulnerable to Regular Expression Denial of Service via multiple route parameters",
|
||||
"range": "<0.1.13"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-9wv6-86v2-598j",
|
||||
"title": "path-to-regexp outputs backtracking regular expressions",
|
||||
"range": "<0.1.10"
|
||||
},
|
||||
{
|
||||
"pkg": "path-to-regexp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rhx6-c78j-4q9w",
|
||||
"title": "path-to-regexp contains a ReDoS",
|
||||
"range": "<0.1.12"
|
||||
},
|
||||
{
|
||||
"pkg": "picomatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-3v7f-55p6-f55p",
|
||||
"title": "Picomatch: Method Injection in POSIX Character Classes causes incorrect Glob Matching",
|
||||
"range": "<2.3.2"
|
||||
},
|
||||
{
|
||||
"pkg": "picomatch",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-c2c7-rcm5-vvqj",
|
||||
"title": "Picomatch has a ReDoS vulnerability via extglob quantifiers",
|
||||
"range": "<2.3.2"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-6g55-p6wh-862q",
|
||||
"title": "PostCSS: Arbitrary file read and information disclosure via attacker-controlled sourceMappingURL in CSS comments",
|
||||
"range": "<=8.5.11"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-fxqj-rqcc-2cmp",
|
||||
"title": "PostCSS: incomplete fix of GHSA-6g55-p6wh-862q — attacker-controlled sourceMappingURL reads arbitrary .map files when `from` is unset",
|
||||
"range": "<=8.5.22"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qx2v-qp2m-jg93",
|
||||
"title": "PostCSS has XSS via Unescaped </style> in its CSS Stringify Output",
|
||||
"range": "<8.5.10"
|
||||
},
|
||||
{
|
||||
"pkg": "postcss",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-r28c-9q8g-f849",
|
||||
"title": "PostCSS: Path Traversal in Previous Source Map Auto-Loading (sourceMappingURL) leads to Arbitrary .map File Disclosure",
|
||||
"range": "<=8.5.17"
|
||||
},
|
||||
{
|
||||
"pkg": "probe-image-size",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-gjj5-9665-rwrc",
|
||||
"title": "probe-image-size: Quadratic-time Denial of Service in the SVG Parser",
|
||||
"range": "<=7.3.0"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-5c6j-r48x-rmvq",
|
||||
"title": "Serialize JavaScript is Vulnerable to RCE via RegExp.flags and Date.prototype.toISOString()",
|
||||
"range": "<=7.0.2"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-m5mm-p8h7-m29v",
|
||||
"title": "Serialize JavaScript: Cross-site scripting (XSS) via unescaped </script at the end of a serialized function body",
|
||||
"range": "<7.1.3"
|
||||
},
|
||||
{
|
||||
"pkg": "serialize-javascript",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-qj8w-gfj5-8c6v",
|
||||
"title": "Serialize JavaScript has CPU Exhaustion Denial of Service via crafted array-like objects",
|
||||
"range": ">=5.0.0 <7.0.5"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-f88m-g3jw-g9cj",
|
||||
"title": "sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591",
|
||||
"range": "<0.35.0"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-rgj7-g3m4-5g8c",
|
||||
"title": "sharp: Vulnerabilities in libheif: GHSA-g89c-p67h-r497 and GHSA-2jg2-4ch7-h545",
|
||||
"range": "<0.35.4"
|
||||
},
|
||||
{
|
||||
"pkg": "sharp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-wq5f-xc86-pv6w",
|
||||
"title": "sharp : Vulnerability in librsvg dependency CVE-2026-96889",
|
||||
"range": "<0.35.5"
|
||||
},
|
||||
{
|
||||
"pkg": "socket.io-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2m8v-j782-fhvr",
|
||||
"title": "Socket.IO: Zero-attachment Memory Exhaustion",
|
||||
"range": ">=4.0.0 <4.2.7"
|
||||
},
|
||||
{
|
||||
"pkg": "socket.io-parser",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-677m-j7p3-52f9",
|
||||
"title": "socket.io allows an unbounded number of binary attachments",
|
||||
"range": ">=4.0.0 <4.2.6"
|
||||
},
|
||||
{
|
||||
"pkg": "source-map-js",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-68fv-2mgg-jv7q",
|
||||
"title": "source-map-js allows event-loop denial of service through indexed source-map section offsets",
|
||||
"range": ">=1.0.0 <1.2.2"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-2p49-hgcm-8545",
|
||||
"title": "SVGO removeScripts plugin leaves some executable scripts intact",
|
||||
"range": ">=1.0.0 <2.8.3"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-4vpr-x523-8j87",
|
||||
"title": "SVGO: removeScripts incompletely sanitizes executable HTML in SVG foreignObject elements",
|
||||
"range": ">=1.0.0 <2.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-w27v-7q3p-w38r",
|
||||
"title": "SVGO: removeScripts allows executable links through namespace and control-character bypasses",
|
||||
"range": ">=1.0.0 <2.8.4"
|
||||
},
|
||||
{
|
||||
"pkg": "svgo",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-xpqw-6gx7-v673",
|
||||
"title": "SVGO DoS through entity expansion in DOCTYPE (Billion Laughs)",
|
||||
"range": ">=2.1.0 <2.8.1"
|
||||
},
|
||||
{
|
||||
"pkg": "tmp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-52f5-9888-hmc6",
|
||||
"title": "tmp allows arbitrary temporary file / directory write via symbolic link `dir` parameter",
|
||||
"range": "<=0.2.3"
|
||||
},
|
||||
{
|
||||
"pkg": "tmp",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-ph9p-34f9-6g65",
|
||||
"title": "tmp has Path Traversal via unsanitized prefix/postfix that enables directory escape",
|
||||
"range": "<0.2.6"
|
||||
},
|
||||
{
|
||||
"pkg": "webpack-dev-middleware",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-g84c-rxfj-3j2c",
|
||||
"title": "webpack-dev-middleware vulnerable to Path Traversal via non-slash-terminated publicPath",
|
||||
"range": "<7.4.5"
|
||||
},
|
||||
{
|
||||
"pkg": "ws",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-58qx-3vcg-4xpx",
|
||||
"title": "ws: Uninitialized memory disclosure",
|
||||
"range": ">=8.0.0 <8.20.1"
|
||||
},
|
||||
{
|
||||
"pkg": "ws",
|
||||
"severity": "high",
|
||||
"advisory": "GHSA-96hv-2xvq-fx4p",
|
||||
"title": "ws: Memory exhaustion DoS from tiny fragments and data chunks",
|
||||
"range": ">=8.0.0 <8.21.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Audit gate: fail the build on any advisory that is not a tracked exception.
|
||||
*
|
||||
* `npm audit --audit-level=<x>` is all-or-nothing, and this tree cannot pass a
|
||||
* flat gate: the Gatsby 5 build toolchain carries ~100 advisories at
|
||||
* low/moderate/high with no non-breaking fix, because gatsby is a runtime
|
||||
* dependency of the build. Blocking on any of those stops publishing
|
||||
* permanently; ignoring the tree lets a genuinely new advisory through. So the
|
||||
* known set is committed to audit-baseline.json with an owner and a reason,
|
||||
* and this script fails only when something NEW appears at or above the
|
||||
* threshold.
|
||||
*
|
||||
* Usage: node scripts/audit-gate.mjs [--write-baseline]
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINE = join(HERE, 'audit-baseline.json');
|
||||
const BLOCKING = new Set(['critical', 'high']);
|
||||
|
||||
/** npm audit exits non-zero when it finds anything, so ignore the status. */
|
||||
function auditReport() {
|
||||
let out;
|
||||
try {
|
||||
out = execFileSync('npm', ['audit', '--json', '--omit=optional'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
maxBuffer: 32 * 1024 * 1024
|
||||
});
|
||||
} catch (err) {
|
||||
out = err.stdout;
|
||||
if (!out) throw err;
|
||||
}
|
||||
return JSON.parse(out);
|
||||
}
|
||||
|
||||
/** Flatten the report to one key per package/severity/advisory triple. */
|
||||
function currentFindings(report) {
|
||||
const findings = new Map();
|
||||
for (const [pkg, v] of Object.entries(report.vulnerabilities ?? {})) {
|
||||
if (!BLOCKING.has(v.severity)) continue;
|
||||
const advisories = (v.via ?? []).filter((x) => typeof x === 'object');
|
||||
if (advisories.length === 0) {
|
||||
// via is a chain of parent package names - attribute to the package.
|
||||
findings.set(`${pkg}|${v.severity}|transitive`, { pkg, severity: v.severity, advisory: 'transitive' });
|
||||
continue;
|
||||
}
|
||||
for (const a of advisories) {
|
||||
const id = (a.url ?? '').split('/').pop() || a.title || 'unknown';
|
||||
findings.set(`${pkg}|${v.severity}|${id}`, { pkg, severity: v.severity, advisory: id, title: a.title, range: a.range });
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
const report = auditReport();
|
||||
const current = currentFindings(report);
|
||||
|
||||
if (process.argv.includes('--write-baseline')) {
|
||||
const accepted = [...current.values()].sort((a, b) => `${a.pkg}${a.advisory}`.localeCompare(`${b.pkg}${b.advisory}`));
|
||||
writeFileSync(
|
||||
BASELINE,
|
||||
JSON.stringify(
|
||||
{
|
||||
$comment:
|
||||
'Advisories at critical/high accepted for this tree. scripts/audit-gate.mjs fails the build on anything not listed here, so removing an entry re-arms the gate for that advisory. Regenerate with: node scripts/audit-gate.mjs --write-baseline',
|
||||
owner: 'Site maintainers (allan@4gl.io)',
|
||||
reason:
|
||||
'Gatsby 5 build toolchain: no non-breaking fix available upstream, and these are build-time dependencies of a statically generated site. Re-check on each Gatsby major bump.',
|
||||
accepted
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + '\n'
|
||||
);
|
||||
console.log(`wrote ${accepted.length} accepted advisories to ${BASELINE}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const baseline = JSON.parse(readFileSync(BASELINE, 'utf8'));
|
||||
const accepted = new Set((baseline.accepted ?? []).map((a) => `${a.pkg}|${a.severity}|${a.advisory}`));
|
||||
|
||||
const fresh = [...current.entries()].filter(([key]) => !accepted.has(key));
|
||||
const resolved = [...accepted].filter((key) => !current.has(key));
|
||||
|
||||
for (const [, f] of fresh) {
|
||||
console.error(`NEW ${f.severity.toUpperCase()}: ${f.pkg} - ${f.title ?? f.advisory} (${f.advisory})`);
|
||||
}
|
||||
|
||||
if (resolved.length > 0) {
|
||||
// Not a failure: a bumped or removed dependency closes advisories, and the
|
||||
// baseline should be trimmed in the same change that closes them.
|
||||
console.log(`${resolved.length} baseline entries no longer present - consider re-running with --write-baseline:`);
|
||||
for (const key of resolved) console.log(` stale: ${key}`);
|
||||
}
|
||||
|
||||
if (fresh.length > 0) {
|
||||
console.error(`\n${fresh.length} untracked advisory/ies at critical or high.`);
|
||||
console.error('Fix it, or add it to scripts/audit-baseline.json with a reason and an owner.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`audit gate: no new critical/high advisories (${current.size} tracked in baseline)`);
|
||||
@@ -21,11 +21,6 @@ const Footer = () => (
|
||||
on <StyledAnchor href="https://sasapps.io">SAS Web Apps</StyledAnchor>
|
||||
.
|
||||
</StyledDesc>
|
||||
<StyledDesc>
|
||||
<StyledAnchor href="https://sasapps.io/cyber-essentials-certified/">
|
||||
Cyber Essentials Certified
|
||||
</StyledAnchor>
|
||||
</StyledDesc>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StyledHeading>Source Code</StyledHeading>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'gatsby'
|
||||
import { FaShieldAlt } from 'react-icons/fa'
|
||||
|
||||
import { Hero, HeroHeading, HeroDesc, HeroBadge } from './style'
|
||||
import { Hero, HeroHeading, HeroDesc } from './style'
|
||||
import { BottomSectionArrow, OutlineButton } from '../shared/styledComponents'
|
||||
import { Container } from '../shared'
|
||||
|
||||
@@ -20,18 +19,9 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||
<HeroHeading>{heading}</HeroHeading>
|
||||
<HeroDesc>{desc}</HeroDesc>
|
||||
{location.pathname === pathPrefix + '/' && (
|
||||
<>
|
||||
<HeroBadge
|
||||
href="https://sasapps.io/cyber-essentials-certified/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<FaShieldAlt /> Cyber Essentials Certified
|
||||
</HeroBadge>
|
||||
<Link to="/contact/">
|
||||
<OutlineButton>Try Data Controller</OutlineButton>
|
||||
</Link>
|
||||
</>
|
||||
<Link to="/contact/">
|
||||
<OutlineButton>Try Data Controller</OutlineButton>
|
||||
</Link>
|
||||
)}
|
||||
</Container>
|
||||
<BottomSectionArrow />
|
||||
|
||||
@@ -17,20 +17,3 @@ export const HeroHeading = styled.h1`
|
||||
export const HeroDesc = styled.p`
|
||||
opacity: 0.8;
|
||||
`
|
||||
|
||||
export const HeroBadge = styled.a`
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin: 0.75rem 0 0;
|
||||
padding: 0.375rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 0.25rem;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: white;
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { FaShieldAlt } from 'react-icons/fa'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledCertBadge = styled.a`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
border: 2px solid #314351;
|
||||
border-radius: 0.25rem;
|
||||
color: #314351;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #314351;
|
||||
}
|
||||
`
|
||||
|
||||
const CertBadge = () => (
|
||||
<StyledCertBadge
|
||||
href="https://sasapps.io/cyber-essentials-certified/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<FaShieldAlt /> Cyber Essentials Certified
|
||||
</StyledCertBadge>
|
||||
)
|
||||
|
||||
export default CertBadge
|
||||
@@ -14,7 +14,7 @@ const NotFound: React.FC<PageProps<unknown, PageContext>> = ({
|
||||
pageContext
|
||||
}) => (
|
||||
<Layout location={location} heroSection={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<SideBar
|
||||
pageContext={pageContext}
|
||||
location={location}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section } from '../components/shared'
|
||||
import CertBadge from '../components/shared/certBadge'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
@@ -17,7 +18,7 @@ const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
heading="Behind Data Controller"
|
||||
desc="Data Controller is a product of 4GL, a brand of Bowe IO Ltd"
|
||||
>
|
||||
<Section color="black" bgColor="white">
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="offset-md-2 col-md-8">
|
||||
<SectionHeading center="no">About Data Controller</SectionHeading>
|
||||
@@ -37,6 +38,7 @@ const About: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
</a>
|
||||
.
|
||||
</SectionDesc>
|
||||
<CertBadge />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -58,7 +58,7 @@ const Contact: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
heading="Book a Demo"
|
||||
desc="Schedule a demonstration of the Data Controller to see our latest features and explore use cases"
|
||||
>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-6">
|
||||
<SectionHeading center="no">Schedule a Call</SectionHeading>
|
||||
|
||||
@@ -20,7 +20,7 @@ const Faq: React.FC<PageProps<unknown>> = ({ location }) => {
|
||||
heading="Data Controller – FAQ"
|
||||
desc="Most common user questions and answers regarding the Data Controller"
|
||||
>
|
||||
<Section color="black" bgColor="white">
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<FAQ
|
||||
|
||||
@@ -7,6 +7,7 @@ import Layout from '../components/layout'
|
||||
import Seo from '../components/seo'
|
||||
|
||||
import { Section, ScheduleDemo } from '../components/shared'
|
||||
import CertBadge from '../components/shared/certBadge'
|
||||
import {
|
||||
SectionHeading,
|
||||
SectionDesc
|
||||
@@ -67,7 +68,7 @@ const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
||||
heading="Flexible and Secure Data Modification"
|
||||
desc="Data Controller for SAS® enables business users to update IT owned source tables, secured by review / approve workflow and full audit trail."
|
||||
>
|
||||
<Section color="black" bgColor="white">
|
||||
<Section color="black" bgColor="white">
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Art src={leftArt} info="Business Owner" />
|
||||
@@ -98,6 +99,7 @@ const Home: React.FC<PageProps<IndexPageData>> = ({ data, location }) => {
|
||||
perform manual data uploads into their preferred database, in
|
||||
real-time, with full validation, approval, security, and control.
|
||||
</SectionDesc>
|
||||
<CertBadge />
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<Art src={rightArt} info="Clinical Research Data" />
|
||||
|
||||
@@ -69,7 +69,7 @@ const BlogListTemplate: React.FC<PageProps<BlogListData, PageContext>> = ({
|
||||
heading="Data Controller’s Knowledge Base"
|
||||
desc="A section dedicated to news, updates and educational pieces."
|
||||
>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
|
||||
@@ -71,7 +71,7 @@ const BlogSearchTemplate: React.FC<PageProps<SearchData, PageContext>> = ({
|
||||
heading="Data Controller’s Knowledge Base"
|
||||
desc="A section dedicated to news, updates and educational pieces."
|
||||
>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
|
||||
@@ -68,7 +68,7 @@ const FeedListTemplate: React.FC<PageProps<FeedListData, PageContext>> = ({
|
||||
heading="Data Controller’s Feed"
|
||||
desc="Announcements and social media style updates from the Data Controller team."
|
||||
>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<Section color="black" bgColor="white" bottomArrow={false}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<div className="row">
|
||||
|
||||
@@ -118,6 +118,22 @@ const StyledContent = styled.div`
|
||||
line-height: 2em;
|
||||
font-family: Monaco, 'Andale Mono', 'Courier New', Courier, monospace;
|
||||
}
|
||||
/* A fenced block tagged with the 'poem' language is verse, not code: drop
|
||||
the code chrome and set it in the serif face, keeping the author's line
|
||||
breaks and blank lines (which read as stanza breaks). */
|
||||
pre:has(> code.language-poem) {
|
||||
background-image: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 1.8rem 0;
|
||||
line-height: 1.75;
|
||||
}
|
||||
pre > code.language-poem {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 1.05rem;
|
||||
color: #222222;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`
|
||||
|
||||
interface PostProps {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>One Table, Two Rule Sets has moved</title>
|
||||
<link rel="canonical" href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta http-equiv="refresh" content="0; url=https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/" />
|
||||
<meta name="robots" content="noindex" />
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
This page has moved to
|
||||
<a href="https://docs.datacontroller.io/developer-guide/two-rule-sets-one-table/">docs.datacontroller.io</a>.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||