Skip to content

Commit

Permalink
feat: plugin renaming & deepFactor for cost analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
thibaudlabat committed Aug 1, 2022
1 parent cc10ce1 commit 30be078
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 81 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

- [Character Limit](#character-limit)
- [Limit Query Cost](#cost-analysis)
- [Disable Introspection](#introspection)
- [Disable Introspection](#block-introspection)
- [Disable Field Suggestion](#field-suggestion)


Expand Down Expand Up @@ -89,15 +89,15 @@ Goto [Events/onPluginUpdate](#onpluginupdate) for more information.
}
```

### Introspection
### Block Introspection

`Introspection plugin` will prevent introspection queries from being executed.
`BlockIntrospection plugin` will prevent introspection queries from being executed.

By default, introspection is still available for our [Live GraphQL Security Testing Platform](https://escape.tech) by providing a valid identifier.

```typescript
{
Introspection: {
BlockIntrospection: {
enabled: true,
options: {
headersWhitelist: {
Expand Down
8 changes: 2 additions & 6 deletions examples/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,9 @@ const armor = new GQLArmor({
options: {
maxLength: 10000,
}
}, Introspection: {
}, BlockIntrospection: {
enabled: false,
},
Profiler:
{
enabled: true,
}
}
}, (status: string, plugin: any) => {
console.log(status, plugin._namespace);
},);
Expand Down
15 changes: 9 additions & 6 deletions lib/graphql-validation-complexity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class ComplexityVisitor {
private readonly scalarCost: number;
private readonly objectCost: number;
private readonly listFactor: number;
private readonly depthFactor: number;
private readonly introspectionListFactor: number;
private costFactor: number;
private cost: number;
Expand All @@ -91,13 +92,14 @@ export default class ComplexityVisitor {
private isIntrospectionQuery = false; // this is not config ! leave it to false.
constructor(
context: ValidationContext,
options: { scalarCost: number; objectCost: number; listFactor: number; introspectionListFactor: number; },
options: { scalarCost: number; objectCost: number; listFactor: number; introspectionListFactor: number; depthFactor:number; },
) {
this.context = context;

this.scalarCost = options.scalarCost;
this.objectCost = options.objectCost;
this.listFactor = options.listFactor;
this.depthFactor = options.depthFactor;
this.introspectionListFactor = options.introspectionListFactor;

this.costFactor = 1;
Expand All @@ -109,6 +111,7 @@ export default class ComplexityVisitor {
};
this.FragmentDefinition = () => {
// don't visit any further we will include these at the spread location
// (???)
return false;
};

Expand Down Expand Up @@ -146,13 +149,13 @@ export default class ComplexityVisitor {
}

this.cost += this.costFactor * this.getFieldCost();
;

}

leaveField(obj) {

if (this.path[this.path.length - 1] !== obj.name.value)
throw Error("shouldnt happen"); // TODO : pas d'erreur + plus explicite?
throw Error("error; shouldn't happen"); // TODO : pas d'erreur + plus explicite?

this.path.pop();

Expand All @@ -173,8 +176,8 @@ export default class ComplexityVisitor {
if (directiveCostFactor != null) {
return directiveCostFactor;
}

return this.getTypeCostFactor(this.context.getType());
const costFactor = this.getTypeCostFactor(this.context.getType());
return costFactor;
}

getTypeCostFactor(type) {
Expand All @@ -189,7 +192,7 @@ export default class ComplexityVisitor {
return typeListFactor * this.getTypeCostFactor(type.ofType);
}

return 1;
return this.depthFactor;
}

isIntrospectionList({ofType}) {
Expand Down
10 changes: 4 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { GQLArmorConfig, PluginConfig } from './types';
import { DefaultCharacterLimitConfig } from './plugins/CharacterLimit';
import { DefaultCostAnalysisConfig } from './plugins/CostAnalysis';
import { DefaultIntrospectionConfig } from './plugins/Introspection';
import { DefaultFieldSuggestionConfig } from './plugins/FieldSuggestion';
import { DefaultProfilerConfig } from './plugins/Profiler';
import { DefaultBlockIntrospectionConfig } from './plugins/BlockIntrospection';
import { DefaultBlockFieldSuggestionConfig } from './plugins/BlockFieldSuggestion';

const defaultConfig: GQLArmorConfig = {
CharacterLimit: DefaultCharacterLimitConfig, // 0x1
CostAnalysis: DefaultCostAnalysisConfig, // 0x2
Introspection: DefaultIntrospectionConfig, // 0x4
FieldSuggestion: DefaultFieldSuggestionConfig, // 0x8
Profiler: DefaultProfilerConfig, // 0x10
BlockIntrospection: DefaultBlockIntrospectionConfig, // 0x4
BlockFieldSuggestion: DefaultBlockFieldSuggestionConfig, // 0x8
};

function applyBitwisePermissions(config: GQLArmorConfig, permUID: number): GQLArmorConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { ArmorPlugin } from '../ArmorPlugin';
import { ValidationRule, GraphQLError, ValidationContext, ASTVisitor, FieldNode } from 'graphql';
import { PluginConfig } from 'types';

export type FieldSuggestionConfig = {
FieldSuggestion?: {} & PluginConfig;
export type BlockFieldSuggestionConfig = {
BlockFieldSuggestion?: {} & PluginConfig;
};
export const DefaultFieldSuggestionConfig = {
_namespace: 'FieldSuggestion',
export const DefaultBlockFieldSuggestionConfig = {
_namespace: 'BlockFieldSuggestion',
enabled: true,
};

const rule = ({}: PluginConfig): ValidationRule => {
return function FieldSuggestion(ctx: ValidationContext): ASTVisitor {
return function BlockFieldSuggestion(ctx: ValidationContext): ASTVisitor {
return {
Field(node: FieldNode) {
const type = ctx.getParentType();
Expand All @@ -29,7 +29,7 @@ const rule = ({}: PluginConfig): ValidationRule => {
};
};

export class FieldSuggestion extends ArmorPlugin {
export class BlockFieldSuggestion extends ArmorPlugin {
getValidationRules(): ValidationRule[] {
return [rule(this.getConfig())];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ArmorPlugin } from '../ArmorPlugin';
import { PluginConfig, PluginDefinition } from '../types';

export type IntrospectionConfig = { Introspection?: PluginConfig };
export const DefaultIntrospectionConfig = {
_namespace: 'Introspection',
export type BlockIntrospectionConfig = { BlockIntrospection?: PluginConfig };
export const DefaultBlockIntrospectionConfig = {
_namespace: 'BlockIntrospection',
enabled: true,
options: {
headersWhitelist: {
Expand Down Expand Up @@ -36,7 +36,7 @@ const plugin = ({ options: { headersWhitelist } }: PluginConfig): PluginDefiniti
};
};

export class Introspection extends ArmorPlugin {
export class BlockIntrospection extends ArmorPlugin {
getApolloPlugins(): PluginDefinition[] {
return [plugin(this.getConfig())];
}
Expand Down
81 changes: 41 additions & 40 deletions src/plugins/CostAnalysis.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,53 @@
import {ArmorPlugin} from '../ArmorPlugin';
import {ValidationRule, PluginConfig} from '../types';
import {ASTVisitor, GraphQLError, TypeInfo, visit, visitWithTypeInfo} from 'graphql';
import { ArmorPlugin } from '../ArmorPlugin';
import { ValidationRule, PluginConfig } from '../types';
import { ASTVisitor, GraphQLError, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
import ComplexityVisitor from '../../lib/graphql-validation-complexity';

export type CostAnalysisConfig = {
CostAnalysis?: { options: { maxCost: number } } & PluginConfig;
CostAnalysis?: { options: { maxCost: number } } & PluginConfig;
};
export const DefaultCostAnalysisConfig = {
_namespace: 'CostAnalysis',
enabled: true,
options: {
maxCost: 1000,
scalarCost: 1,
objectCost: 1,
listFactor: 10,
// Special list factor to make schema queries not have huge costs.
introspectionListFactor: 2,
},
_namespace: 'CostAnalysis',
enabled: true,
options: {
maxCost: 1000,
scalarCost: 1,
objectCost: 1,
listFactor: 3,
depthFactor: 1.5,
// Special list factor to make schema queries not have huge costs.
introspectionListFactor: 2,
},
};

const rule = ({options}: PluginConfig): ValidationRule =>
function ComplexityLimit(context) {
const visitor = new ComplexityVisitor(context, options);
// @ts-ignore
const typeInfo = context._typeInfo || new TypeInfo(context.getSchema());
const rule = ({ options }: PluginConfig): ValidationRule =>
function ComplexityLimit(context) {
const visitor = new ComplexityVisitor(context, options);
// @ts-ignore
const typeInfo = context._typeInfo || new TypeInfo(context.getSchema());

return {
Document: {
enter(node) {
visit(node, visitWithTypeInfo(typeInfo, visitor as ASTVisitor));
},
leave(node) {
const cost = visitor.getCost();
console.log(`COST: ${cost}`);
if (cost > options.maxCost) {
context.reportError(
new GraphQLError('query exceeds complexity limit', {
nodes: [node],
}),
);
}
},
},
};
return {
Document: {
enter(node) {
visit(node, visitWithTypeInfo(typeInfo, visitor as ASTVisitor));
},
leave(node) {
const cost = visitor.getCost();
console.log(`COST: ${cost}`);
if (cost > options.maxCost) {
context.reportError(
new GraphQLError('query exceeds complexity limit', {
nodes: [node],
}),
);
}
},
},
};
};

export class CostAnalysis extends ArmorPlugin {
getValidationRules(): ValidationRule[] {
return [rule(this.getConfig())];
}
getValidationRules(): ValidationRule[] {
return [rule(this.getConfig())];
}
}
6 changes: 3 additions & 3 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CharacterLimit } from './CharacterLimit';
import { Introspection } from './Introspection';
import { BlockIntrospection } from './BlockIntrospection';
import { CostAnalysis } from './CostAnalysis';
import { FieldSuggestion } from './FieldSuggestion';
import { BlockFieldSuggestion } from './BlockFieldSuggestion';

export { CharacterLimit, Introspection, CostAnalysis, FieldSuggestion };
export { CharacterLimit, BlockIntrospection, CostAnalysis, BlockFieldSuggestion };
10 changes: 4 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
import { ASTVisitor, ValidationContext } from 'graphql';
import { FieldSuggestionConfig } from 'plugins/FieldSuggestion';
import { BlockFieldSuggestionConfig } from 'plugins/BlockFieldSuggestion';
import { CharacterLimitConfig } from './plugins/CharacterLimit';
import { CostAnalysisConfig } from './plugins/CostAnalysis';
import { IntrospectionConfig } from './plugins/Introspection';
import { DefaultProfilerConfig, ProfilerConfig } from './plugins/Profiler';
import { BlockIntrospectionConfig } from './plugins/BlockIntrospection';

export type PluginDefinition = ApolloServerPlugin | (() => ApolloServerPlugin); // apollo-server-core/src/types.ts
export type ValidationRule = (context: ValidationContext) => ASTVisitor;
Expand All @@ -26,8 +25,7 @@ export type PluginConfig = {
};

export type GQLArmorConfig =
| IntrospectionConfig
| BlockIntrospectionConfig
| CharacterLimitConfig
| CostAnalysisConfig
| FieldSuggestionConfig
| ProfilerConfig;
| BlockFieldSuggestionConfig;

0 comments on commit 30be078

Please sign in to comment.