Skip to content

Commit 3682e1d

Browse files
akshay-vizCopilot
andcommitted
fix(model-apps): stop generic prefixes from waving real environments through
Caught by Copilot in review. `PLACEHOLDER_ROOTS` is matched as a PREFIX, so every entry waved through an unbounded family of names -- and four of the entries were generic English words. `test`, `demo`, `sample` and `my-` meant an environment genuinely called `TestEnv01`, `demo-prod-01`, `sampleorg99` or a tenant `my-real-tenant` was declared a placeholder and its live URL could be committed to this public repo. That is the failure direction that matters for this guard. A false NEGATIVE is the leak it exists to prevent; a false positive is a one-line fix by whoever hits it. Generic words are also exactly the prefix real Power Platform environments carry, so these were the worst possible entries in a prefix list. Removed all four. Nothing in tree depended on them: the only in-scope subdomain that matched was bare `test`, which is four characters and already passes via the length ceiling. `contoso`, `fabrikam` and `example` stay -- they are Microsoft's documented fictitious organizations, so a name built on them reads as fake even when long -- along with `your-`, whose trailing hyphen marks it as an instructional template rather than a name anyone would deploy. Four tests, red-green verified: restoring the generic roots fails the two that pin the new behaviour. They assert both directions -- `testenv12345` and `testtenant0042` are now rejected, while bare `test`/`demo`/`dev` still pass via the length rule and the fictional brands still match as prefixes. 1480 tests, 6/6 validators, and the guard still exits 0 against the repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 42626da2-b66f-4162-acaa-b1127ef23d89
1 parent 019bbdd commit 3682e1d

2 files changed

Lines changed: 156 additions & 111 deletions

File tree

Lines changed: 147 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,147 @@
1-
// Guards the guard. The point of validate-no-real-environments.js is to fail when a
2-
// real environment identifier is committed to this PUBLIC repository, so the tests
3-
// that matter most are the ones asserting it does NOT stay silent on real inputs.
4-
// Every "real" case below is an identifier that was genuinely committed to this repo
5-
// and removed, or a shape the Dataverse service actually generates.
6-
7-
const test = require('node:test');
8-
const assert = require('node:assert');
9-
10-
const {
11-
isPlaceholder,
12-
scanText,
13-
} = require('../validate-no-real-environments.js');
14-
15-
test('placeholder subdomains are accepted', () => {
16-
for (const value of [
17-
'contoso',
18-
'contoso-dev',
19-
'contosobapenv0001',
20-
'fabrikamenv001',
21-
'example',
22-
'test',
23-
'your-env',
24-
'org',
25-
'org1',
26-
'ORG2',
27-
'other',
28-
'x',
29-
]) {
30-
assert.equal(isPlaceholder(value), true, `${value} should be treated as a placeholder`);
31-
}
32-
});
33-
34-
test('real environment names are rejected', () => {
35-
for (const value of [
36-
// Previously committed to this repository.
37-
'aurorabapenv610b3',
38-
'aurorabapenv4ab3f',
39-
'tmsbapenv5ee52',
40-
// Shapes seen elsewhere in the repo / generated by Dataverse.
41-
'org1e98cc97',
42-
'pascalepipelineshost',
43-
'1841communityv2fresh',
44-
]) {
45-
assert.equal(isPlaceholder(value), false, `${value} should be rejected`);
46-
}
47-
});
48-
49-
test('the org<8hex> shape is rejected even though it starts with the allowed word "org"', () => {
50-
// Regression guard for the ordering inside isPlaceholder: the hex-shape check must
51-
// run BEFORE the placeholder-root and length checks, otherwise a real auto-generated
52-
// Dataverse org is rescued by the generic "org" allowance.
53-
assert.equal(isPlaceholder('org1e98cc97'), false);
54-
assert.equal(isPlaceholder('orgc4f78248'), false);
55-
// ...while short generic org stand-ins still pass.
56-
assert.equal(isPlaceholder('org123'), true);
57-
});
58-
59-
test('a real Dataverse host in text is reported', () => {
60-
const violations = scanText('- URL: https://aurorabapenv610b3.crmtest.dynamics.com/');
61-
assert.ok(violations.length > 0, 'expected at least one violation');
62-
assert.ok(
63-
violations.some((v) => v.detail.includes('aurorabapenv')),
64-
`expected a banned-token violation, got ${JSON.stringify(violations)}`,
65-
);
66-
});
67-
68-
test('a real tenant in a UPN is reported', () => {
69-
const violations = scanText('user: someone@capintegration01.onmicrosoft.com');
70-
assert.ok(
71-
violations.some((v) => v.detail.includes('capintegration')),
72-
`expected a banned-token violation, got ${JSON.stringify(violations)}`,
73-
);
74-
});
75-
76-
test('a previously-unseen real org host is reported by shape alone', () => {
77-
// This one is NOT on the banned-token list, so it can only be caught by the shape
78-
// rule. Without that rule the guard would only ever re-catch yesterday's leak.
79-
const violations = scanText('envUrl: https://org4a2942d9.crm17.dynamics.com');
80-
assert.equal(violations.length, 1, JSON.stringify(violations));
81-
assert.match(violations[0].detail, /non-placeholder Dataverse host/);
82-
});
83-
84-
test('placeholder-only content produces no violations', () => {
85-
const clean = [
86-
'https://contoso.crm.dynamics.com',
87-
'maker@contoso.onmicrosoft.com',
88-
'https://contosobapenv0001.crmtest.dynamics.com/',
89-
'tester@fabrikamtenant01.onmicrosoft.com',
90-
'https://org.crm.dynamics.com',
91-
].join('\n');
92-
assert.deepEqual(scanText(clean), []);
93-
});
94-
95-
test('violations carry the 1-based line number', () => {
96-
const violations = scanText(['clean line', '', 'https://org9cf0ed45.crm.dynamics.com'].join('\n'));
97-
assert.equal(violations.length, 1);
98-
assert.equal(violations[0].line, 3);
99-
});
100-
101-
test('CRLF content is scanned with correct line numbers', () => {
102-
// Plugin scripts are CRLF while eval fixtures are LF; a split on "\n" alone would
103-
// leave a trailing "\r" and could shift or mangle reported lines.
104-
const violations = scanText('clean\r\nhttps://org5fbe4359.crm5.dynamics.com\r\n');
105-
assert.equal(violations.length, 1);
106-
assert.equal(violations[0].line, 2);
107-
});
1+
// Guards the guard. The point of validate-no-real-environments.js is to fail when a
2+
// real environment identifier is committed to this PUBLIC repository, so the tests
3+
// that matter most are the ones asserting it does NOT stay silent on real inputs.
4+
// Every "real" case below is an identifier that was genuinely committed to this repo
5+
// and removed, or a shape the Dataverse service actually generates.
6+
7+
const test = require('node:test');
8+
const assert = require('node:assert');
9+
10+
const {
11+
isPlaceholder,
12+
scanText,
13+
} = require('../validate-no-real-environments.js');
14+
15+
test('placeholder subdomains are accepted', () => {
16+
for (const value of [
17+
'contoso',
18+
'contoso-dev',
19+
'contosobapenv0001',
20+
'fabrikamenv001',
21+
'example',
22+
'test',
23+
'your-env',
24+
'org',
25+
'org1',
26+
'ORG2',
27+
'other',
28+
'x',
29+
]) {
30+
assert.equal(isPlaceholder(value), true, `${value} should be treated as a placeholder`);
31+
}
32+
});
33+
34+
test('real environment names are rejected', () => {
35+
for (const value of [
36+
// Previously committed to this repository.
37+
'aurorabapenv610b3',
38+
'aurorabapenv4ab3f',
39+
'tmsbapenv5ee52',
40+
// Shapes seen elsewhere in the repo / generated by Dataverse.
41+
'org1e98cc97',
42+
'pascalepipelineshost',
43+
'1841communityv2fresh',
44+
]) {
45+
assert.equal(isPlaceholder(value), false, `${value} should be rejected`);
46+
}
47+
});
48+
49+
test('the org<8hex> shape is rejected even though it starts with the allowed word "org"', () => {
50+
// Regression guard for the ordering inside isPlaceholder: the hex-shape check must
51+
// run BEFORE the placeholder-root and length checks, otherwise a real auto-generated
52+
// Dataverse org is rescued by the generic "org" allowance.
53+
assert.equal(isPlaceholder('org1e98cc97'), false);
54+
assert.equal(isPlaceholder('orgc4f78248'), false);
55+
// ...while short generic org stand-ins still pass.
56+
assert.equal(isPlaceholder('org123'), true);
57+
});
58+
59+
test('a real Dataverse host in text is reported', () => {
60+
const violations = scanText('- URL: https://aurorabapenv610b3.crmtest.dynamics.com/');
61+
assert.ok(violations.length > 0, 'expected at least one violation');
62+
assert.ok(
63+
violations.some((v) => v.detail.includes('aurorabapenv')),
64+
`expected a banned-token violation, got ${JSON.stringify(violations)}`,
65+
);
66+
});
67+
68+
test('a real tenant in a UPN is reported', () => {
69+
const violations = scanText('user: someone@capintegration01.onmicrosoft.com');
70+
assert.ok(
71+
violations.some((v) => v.detail.includes('capintegration')),
72+
`expected a banned-token violation, got ${JSON.stringify(violations)}`,
73+
);
74+
});
75+
76+
test('a previously-unseen real org host is reported by shape alone', () => {
77+
// This one is NOT on the banned-token list, so it can only be caught by the shape
78+
// rule. Without that rule the guard would only ever re-catch yesterday's leak.
79+
const violations = scanText('envUrl: https://org4a2942d9.crm17.dynamics.com');
80+
assert.equal(violations.length, 1, JSON.stringify(violations));
81+
assert.match(violations[0].detail, /non-placeholder Dataverse host/);
82+
});
83+
84+
test('placeholder-only content produces no violations', () => {
85+
const clean = [
86+
'https://contoso.crm.dynamics.com',
87+
'maker@contoso.onmicrosoft.com',
88+
'https://contosobapenv0001.crmtest.dynamics.com/',
89+
'tester@fabrikamtenant01.onmicrosoft.com',
90+
'https://org.crm.dynamics.com',
91+
].join('\n');
92+
assert.deepEqual(scanText(clean), []);
93+
});
94+
95+
test('violations carry the 1-based line number', () => {
96+
const violations = scanText(['clean line', '', 'https://org9cf0ed45.crm.dynamics.com'].join('\n'));
97+
assert.equal(violations.length, 1);
98+
assert.equal(violations[0].line, 3);
99+
});
100+
101+
test('CRLF content is scanned with correct line numbers', () => {
102+
// Plugin scripts are CRLF while eval fixtures are LF; a split on "\n" alone would
103+
// leave a trailing "\r" and could shift or mangle reported lines.
104+
const violations = scanText('clean\r\nhttps://org5fbe4359.crm5.dynamics.com\r\n');
105+
assert.equal(violations.length, 1);
106+
assert.equal(violations[0].line, 2);
107+
});
108+
109+
test('a real environment whose name STARTS with a generic word is rejected', () => {
110+
// Review finding: PLACEHOLDER_ROOTS is matched as a prefix, so generic English words like
111+
// `test`/`demo`/`sample` waved through an unbounded family of names — and environments genuinely
112+
// called `TestEnv01` or `demo-prod-01` are common. A false negative here is a leak, which is the
113+
// failure direction that matters, so those roots were removed.
114+
for (const value of [
115+
'testenv12345',
116+
'TestEnv01',
117+
'demo-prod-01',
118+
'demoorg9931',
119+
'sampleorg99',
120+
'my-real-tenant',
121+
'samplecorp-prod',
122+
]) {
123+
assert.equal(isPlaceholder(value), false, `${value} must NOT be treated as a placeholder`);
124+
}
125+
});
126+
127+
test('bare generic stand-ins still pass, via the length rule rather than a prefix', () => {
128+
// These are what the in-tree fixtures actually use; they are short enough that the length ceiling
129+
// covers them without needing a prefix entry that would also cover `testenv12345`.
130+
for (const value of ['test', 'demo', 'dev', 'stg', 'uat', 'x']) {
131+
assert.equal(isPlaceholder(value), true, `${value} should still be accepted`);
132+
}
133+
});
134+
135+
test('the fictional brands still match as prefixes', () => {
136+
// Contoso/Fabrikam are Microsoft's documented sample organizations, so a name built on them reads
137+
// as obviously fake even when long.
138+
for (const value of ['contosobapenv0001', 'fabrikamtenant01', 'exampleorg', 'your-env']) {
139+
assert.equal(isPlaceholder(value), true, `${value} should be accepted`);
140+
}
141+
});
142+
143+
test('a real-looking tenant beginning with a generic word is reported', () => {
144+
const violations = scanText('user: someone@testtenant0042.onmicrosoft.com');
145+
assert.equal(violations.length, 1, JSON.stringify(violations));
146+
assert.match(violations[0].detail, /non-placeholder tenant/);
147+
});

scripts/validate-no-real-environments.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,20 @@ const SCAN_PATHS = ['plugins/model-apps', 'evals/model-apps'];
4141
// Contoso/Fabrikam as its standard sample organizations, so they read as obviously
4242
// fake to any external reader.
4343
// https://learn.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/term-collections/fictitious-names
44+
//
45+
// Kept deliberately SHORT. These are matched as PREFIXES, so every entry here waves through an
46+
// unbounded family of names — and a generic English word is exactly the kind of prefix a real
47+
// environment carries. `test`, `demo`, `sample` and `my-` were removed for that reason:
48+
// environments genuinely named `TestEnv01`, `demo-prod-01` or `sampleorg99` are common, and a prefix
49+
// rule would have declared each of them a placeholder and let a live URL through. That is the
50+
// failure direction that matters here — a false negative is a leak, while a false positive is a
51+
// one-line fix by whoever hits it. Bare `test`/`demo` still pass via the length rule below, which is
52+
// what the in-tree fixtures actually use.
4453
const PLACEHOLDER_ROOTS = [
4554
'contoso',
4655
'fabrikam',
4756
'example',
48-
'sample',
49-
'test',
50-
'demo',
5157
'your-',
52-
'my-',
5358
];
5459

5560
// Short generic stand-ins used throughout the unit tests (`org`, `x`, `a`, `b`,

0 commit comments

Comments
 (0)