Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
90c9f7f
feat: adds functionality to lint specs with a user uploaded spectral …
fiosman May 4, 2026
837dc49
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 4, 2026
8b96e61
chore: update var name
fiosman May 4, 2026
7092d46
chore: remove log
fiosman May 4, 2026
f307905
feat: adds logic to persist rulesetFilePath
fiosman May 4, 2026
f327dea
feat: adds logic to remove a uploaded ruleset file and use default OA…
fiosman May 4, 2026
de66df4
feat: adds logic to clean up old ruleset file watcher
fiosman May 5, 2026
0f035c5
chore: adds comment for testing
fiosman May 5, 2026
a879379
chore: adds comment for clarity
fiosman May 5, 2026
4146159
feat: adds logic to enable cunstom lint rules for cloud/git sync proj…
fiosman May 6, 2026
224f18d
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 6, 2026
1a83bf9
chore: remove file watcher for now
fiosman May 7, 2026
53673f1
chore: remove file watcher for now
fiosman May 7, 2026
2f14bc7
feat: adds proper logic to handle syncing rulesets for git sync/cloud…
fiosman May 8, 2026
ac53b4e
chore: remove unneeded event
fiosman May 8, 2026
c0ffa96
feat: auto open up the lint pane if there are lint warnings/errors
fiosman May 8, 2026
29074db
chore: update ApiSpec mutations to include rulesetContent as optional…
fiosman May 8, 2026
eb477e1
feat: uses clientAction mutation hook to update db
fiosman May 8, 2026
6643e9f
chore: adds logic to handle file not found
fiosman May 8, 2026
fd9aad1
feat: removes .spectral.yaml file name restriction
fiosman May 11, 2026
99f6758
chore: remove comment
fiosman May 12, 2026
e27c477
feat: adds some utils to validate user provided spectral ruleset file
fiosman May 13, 2026
13990f9
feat: adds view ruleset modal; slight refactoring to clean up code
fiosman May 13, 2026
d4dbc2a
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 13, 2026
af4f28a
feat: fixes some styling
fiosman May 13, 2026
d5fb410
chore: adds some comments
fiosman May 13, 2026
bbe15a0
chore: change function names/clean up
fiosman May 14, 2026
7b14684
feat: adds logic to flatten extended rulesets into inline prior to wr…
fiosman May 14, 2026
fe7d725
chore: update comment
fiosman May 14, 2026
90ae428
chore: clean up
fiosman May 14, 2026
55c58fb
chore: update comment
fiosman May 14, 2026
19430ea
chore: more comments
fiosman May 14, 2026
5a4f636
chore: remove comment
fiosman May 14, 2026
7bc5dc6
refactor: clean up code to make it more readable
fiosman May 14, 2026
d215692
feat: address double writes when uploading
fiosman May 14, 2026
b8f3c81
chore: update comments/error messages
fiosman May 14, 2026
c832d4b
chore: update comment
fiosman May 14, 2026
3df5ca5
test: adds unit tests for spectral ruleset validator
fiosman May 14, 2026
f12b146
test: adds unit tests for spectral ruleset validator
fiosman May 14, 2026
03c96b0
test: add unit tests
fiosman May 15, 2026
c79f465
chore: cleanup code
fiosman May 15, 2026
ab64fba
chore: make comments much clearer
fiosman May 15, 2026
b8a97cf
feat: adds proper error messages when user attempts to upload a rule …
fiosman May 15, 2026
199fc11
feat: adds UI tweaks
fiosman May 15, 2026
7b02375
test: adds unit tests
fiosman May 15, 2026
f703d99
feat: adds proper logic to handle scenarios when file does not exist …
fiosman May 16, 2026
0c5e30b
feat: adds proper logic to handle scenarios when file does not exist …
fiosman May 16, 2026
ed4548e
chore: sight clean up
fiosman May 19, 2026
a343f60
chore: bring back old code
fiosman May 19, 2026
44b663c
feat: adds logic to migrate rulesets when changing project types
fiosman May 19, 2026
b119e5a
chore: update some styles
fiosman May 20, 2026
9e3f07d
chore: adds more styling changes
fiosman May 20, 2026
615a6b2
chore: more styling updates
fiosman May 20, 2026
a3cd2b1
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 20, 2026
4fe9b36
feat: adds the ruleset to project scope instead of work space so git …
fiosman May 20, 2026
a105c23
feat: adds functionality to get syncing project scoped rulesets worki…
fiosman May 20, 2026
df72119
feat: adds logic to sync git FS/DB for project ruleset
fiosman May 21, 2026
ed24a5b
chore: refactor to use nedb as source of truth for rulesetContent for…
fiosman May 21, 2026
23f77e2
feat: adds logic to mitigate against repo replacing cloud project rul…
fiosman May 21, 2026
bea14ee
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 21, 2026
4433ea5
chore: fixes imports order
fiosman May 21, 2026
dc44b9c
test: fixes e2e test
fiosman May 21, 2026
7748f81
fix: resolve aikido security suggestions
Ali-Sab May 21, 2026
2a27c4c
chore: attempt to fix SSRF dns resolve
fiosman May 21, 2026
2e13084
chore: update error messages
fiosman May 22, 2026
dd06a30
test: update tests
fiosman May 22, 2026
c2e4657
chore: move helpers to common so inso can use them
fiosman May 22, 2026
9b46af6
feat: adds functionality to flatten remote urls in spectral extends
fiosman May 22, 2026
9bcf19a
test: update tests
fiosman May 23, 2026
0162ca4
feat: adds logic to validate remote extends before passing ruleset to…
fiosman May 23, 2026
3c5862e
fix: set canDuplicate to false on the ProjectLintRuleset model
fiosman May 24, 2026
20cd0ca
test: adds more test cases for verifying remote extends validations
fiosman May 24, 2026
a3f0aaf
test: adds more test cases for verifying remote extends validations
fiosman May 24, 2026
6ebcf06
chore: disallow redirects when fetching in spectral resolver
fiosman May 24, 2026
e1ef658
chore: adds aria labels for a11y
fiosman May 24, 2026
efe24ad
chore: small clean ups
fiosman May 24, 2026
c5df4d6
test: update test
fiosman May 25, 2026
370213a
feat: adds tooltip verbage to let user know that local file paths are…
fiosman May 25, 2026
719ed09
Merge branch 'develop' into feat/custom-lint-rules
fiosman May 25, 2026
8690026
chore: address PR comments
fiosman May 25, 2026
39de451
chore: add e2e tests for custom lint for OAS
Ali-Sab May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const rendererNodeRestrictionIgnores = [
...rendererNodeMigrationOffenders,
'packages/insomnia/src/common/__tests__/**/*.{ts,tsx}',
'packages/insomnia/src/common/send-request.ts',
'packages/insomnia/src/common/bundle-spectral-ruleset.ts',
];

export default defineConfig([
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions packages/insomnia-inso/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,7 +886,11 @@ export const go = (args?: string[]) => {
)
.command('spec [identifier]')
.description('Lint an API Specification, identifier can be an API Spec id or a file path')
.action(async identifier => {
.option(
'-r, --ruleset <path>',
'path to a Spectral ruleset file, overrides default OAS ruleset and any ruleset in the API Spec folder',
)
.action(async (identifier, cmd: { ruleset?: string }) => {
const options = await mergeOptionsAndInit({});

// Assert identifier is a file
Expand All @@ -899,11 +903,16 @@ export const go = (args?: string[]) => {
const pathToSearch = '';
let specContent: string | undefined;
let rulesetFileName: string | undefined;
if (cmd.ruleset) {
rulesetFileName = getAbsoluteFilePath({ workingDir: options.workingDir, file: cmd.ruleset });
}
if (isIdentifierAFile) {
// try load as a file
logger.trace(`Linting specification file from identifier: \`${identifierAsAbsPath}\``);
specContent = await fs.promises.readFile(identifierAsAbsPath, 'utf8');
rulesetFileName = await getRuleSetFileFromFolderByFilename(identifierAsAbsPath);
if (!rulesetFileName) {
rulesetFileName = await getRuleSetFileFromFolderByFilename(identifierAsAbsPath);
}
if (!specContent) {
logger.fatal(`Specification content not found using path: ${identifier} in ${identifierAsAbsPath}`);
return process.exit(1);
Expand Down
73 changes: 71 additions & 2 deletions packages/insomnia-inso/src/commands/lint-specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,70 @@ import type { RulesetDefinition } from '@stoplight/spectral-core';
import { Spectral } from '@stoplight/spectral-core';

const { bundleAndLoadRuleset } = require('@stoplight/spectral-ruleset-bundler/with-loader');
import dns from 'node:dns/promises';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { Resolver } from '@stoplight/spectral-ref-resolver';
import { oas } from '@stoplight/spectral-rulesets';
import { fetch as spectralFetch } from '@stoplight/spectral-runtime';
import { DiagnosticSeverity } from '@stoplight/types';
import { bundleSpectralRuleset } from 'insomnia/src/common/bundle-spectral-ruleset';
import { isPrivateOrLoopbackHost } from 'insomnia/src/common/private-host';

import { InsoError } from '../errors';
import { logger } from '../logger';

// Protect against SSRF attacks in spec $ref resolution.
// Note: This is duplicated in insomnia's main/lint-process.mjs. Remember to mirror changes there as well.
function isSafeRefUrl(href: string): boolean {
let url: URL;
try {
url = new URL(href);
} catch {
return false;
}
if (url.protocol !== 'https:') {
return false;
}
return Boolean(url.hostname) && !isPrivateOrLoopbackHost(url.hostname.toLowerCase());
}

// Block hosts that resolve to private/loopback addresses (e.g. *.localtest.me → 127.0.0.1),
// Note: This is duplicated in insomnia's main/lint-process.mjs. Remember to mirror changes there as well.
async function assertResolvesToPublicHost(hostname: string): Promise<void> {
const records = await dns.lookup(hostname, { all: true });
for (const { address } of records) {
if (isPrivateOrLoopbackHost(address)) {
throw new Error(`Failed to resolve host. "${hostname}" resolves to a private or loopback address.`);
}
}
}

// Note: This is duplicated in insomnia's main/lint-process.mjs. Remember to mirror changes there as well.
const safeHttpResolver = {
async resolve(ref: { href: () => string }): Promise<string> {
const href = ref.href();
if (!isSafeRefUrl(href)) {
throw new Error(`Failed to resolve "${href}". Only https URLs to public hosts are allowed.`);
}
await assertResolvesToPublicHost(new URL(href).hostname.toLowerCase());
const response = await fetch(href, { redirect: 'error' });
if (!response.ok) {
throw new Error(`Failed to fetch "${href}": ${response.status} ${response.statusText}`);
}
return response.text();
},
};

export const safeRefResolver = new Resolver({
resolvers: {
http: safeHttpResolver,
https: safeHttpResolver,
},
});

export const getRuleSetFileFromFolderByFilename = async (filePath: string) => {
try {
const filesInSpecFolder = await fs.promises.readdir(path.dirname(filePath));
Expand All @@ -31,12 +87,24 @@ export async function lintSpecification({
specContent: string;
rulesetFileName?: string;
}) {
const spectral = new Spectral();
const spectral = new Spectral({ resolver: safeRefResolver });
// Use custom ruleset if present
let ruleset = oas;
try {
if (rulesetFileName) {
ruleset = await bundleAndLoadRuleset(rulesetFileName, { fs });
// Flatten all local extends and validate remote extends (SSRF + disallowed keys)
// before any content reaches Spectral.
const bundledContent = await bundleSpectralRuleset(rulesetFileName);
// bundleAndLoadRuleset requires a file path, so write the pre-validated bundle to
// a uniquely-named temp directory and clean it up immediately after loading.
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'spectral-'));
try {
const tempRulesetPath = path.join(tempDir, '.spectral.yaml');
await fs.promises.writeFile(tempRulesetPath, bundledContent, { encoding: 'utf8' });
ruleset = await bundleAndLoadRuleset(tempRulesetPath, { fs, fetch: spectralFetch });
} finally {
await fs.promises.rm(tempDir, { recursive: true, force: true });
}
}
} catch (error) {
logger.fatal(error.message);
Expand All @@ -45,6 +113,7 @@ export async function lintSpecification({

spectral.setRuleset(ruleset as RulesetDefinition);
const results = await spectral.run(specContent);

if (!results.length) {
logger.log('No linting errors or warnings.');
return { results, isValid: true };
Expand Down
Loading
Loading