Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ plugins for a given scope. Each entry is either:
Valid plugin names: `repository`, `labels`, `collaborators`, `teams`,
`milestones`, `branches`, `autolinks`, `validator`, `rulesets`, `environments`,
`custom_properties`, `custom_repository_roles`, `variables`, `archive`,
`app_installations`.
`app_installations`, `code_scanning`.

#### Strip matrix (which source layers are removed before merge)

Expand Down Expand Up @@ -848,6 +848,40 @@ still selects them.
and [`additive_plugins`](#additive-plugins-additive_plugins). In additive mode
the plugin only **adds** repos to installations and never removes them.

### Code scanning default setup (`code_scanning`)

The `code_scanning` plugin manages a repository's
[CodeQL default setup](https://docs.github.com/en/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning).
Like other repository settings, it can be declared in `settings.yml`, a suborg
file or a repo file, and later layers override earlier ones.

```yml
code_scanning:
default_setup:
state: configured # or not-configured
query_suite: extended # default | extended
threat_model: remote # remote | remote_and_local
languages: # omit to let GitHub analyze the languages it detects
- csharp
- actions
# runner_type: labeled # standard | labeled
# runner_label: code-scanning
```

- Only the fields you declare are compared and sent; the others are left to
GitHub. The order of `languages` does not matter.
- A `languages` list replaces the one inherited from a lower layer instead of
being merged with it, so a suborg or repo can narrow the list, or clear it
with `languages: []`.
- With `state: not-configured`, only the state is compared and applied.
- Private and internal repositories need GitHub Code Security (or GitHub
Advanced Security). Without it GitHub rejects the update, and safe-settings
records the error for that repository and carries on with the others.

The plugin can be turned off with
[`disable_plugins`](#disabling-plugins-disable_plugins). It is not an additive
plugin.

### The Settings Files

The settings files can be used to set the policies at the `org`, `suborg` or `repo` level.
Expand Down
10 changes: 10 additions & 0 deletions docs/sample-settings/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,16 @@ autolinks:
- key_prefix: "MYLINK-"
url_template: "https://mywebsite.com/<num>"

# CodeQL default setup
# See https://docs.github.com/en/rest/code-scanning/code-scanning#update-a-code-scanning-default-setup-configuration for available options
code_scanning:
default_setup:
state: configured
query_suite: default
languages:
- javascript-typescript
- actions

# Repository name validation
validator:
#pattern: '[a-zA-Z0-9_-]+_[a-zA-Z0-9_-]+.*'
Expand Down
132 changes: 132 additions & 0 deletions lib/plugins/code_scanning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const ErrorStash = require('./errorStash')
const NopCommand = require('../nopcommand')

// The config section name. NOP results carry it as their plugin, since the dry-run
// drift filter looks up the section by the lowercased plugin name.
const SECTION = 'code_scanning'
const DEFAULT_SETUP_FIELDS = ['state', 'query_suite', 'threat_model', 'languages', 'runner_type', 'runner_label']

module.exports = class CodeScanning extends ErrorStash {
/**
* The `default_setup.languages` of the highest-priority layer that declares them.
* The generic deep merge concatenates arrays, but a lower layer's languages should be
* replaced, so a repo can narrow or clear the list it inherits.
*
* @param {Array<object|undefined>} sections `code_scanning` sections, lowest priority first
* @returns {string[]|undefined}
*/
static declaredLanguages (sections) {
let languages
for (const section of sections) {
if (Array.isArray(section?.default_setup?.languages)) {
languages = section.default_setup.languages
}
}
return languages
}

constructor (nop, github, repo, settings, log, errors) {
super(errors)
this.github = github
this.repo = repo
this.settings = settings
this.log = log
this.nop = nop
}

/**
* Reconcile the repository's CodeQL default setup with `code_scanning.default_setup`.
*
* @see {@link https://docs.github.com/en/rest/code-scanning/code-scanning#update-a-code-scanning-default-setup-configuration}
*/
async sync () {
this.hasChanges = false
const desired = this.settings?.default_setup
if (!desired) {
return []
}

let current
try {
const { data } = await this.github.rest.codeScanning.getDefaultSetup({ owner: this.repo.owner, repo: this.repo.repo })
current = data
} catch (e) {
return this.handleError('Error getting code scanning default setup', e)
}

const { modifications, deletions } = this.changes(current, desired)
if (Object.keys(modifications).length === 0 && Object.keys(deletions).length === 0) {
this.log.debug(`Code scanning default setup for ${this.repo.owner}/${this.repo.repo} already matches the config`)
return []
}
this.hasChanges = true

const params = { owner: this.repo.owner, repo: this.repo.repo, ...this.declaredFields(desired) }
if (this.nop) {
return [new NopCommand(
SECTION,
this.repo,
this.github.rest.codeScanning.updateDefaultSetup.endpoint(params),
{ msg: 'Change found', additions: {}, modifications, deletions }
)]
}

this.log.info(`Updating code scanning default setup for ${this.repo.owner}/${this.repo.repo}`)
try {
await this.github.rest.codeScanning.updateDefaultSetup(params)
} catch (e) {
return this.handleError('Error updating code scanning default setup', e)
}
return []
}

/**
* The declared fields that differ from GitHub. With `state: not-configured` only the state
* is compared, since GitHub keeps no other settings for a repository without default setup.
*
* As in the Repository plugin, the NOP report reads `deletions` as the before values and
* `modifications` as the after values. Every changed field keeps GitHub's value in
* `deletions`. A field cleared to `[]` or `null` stays out of `modifications`, where the
* report would treat its empty value as no change, and so shows as deleted.
*/
changes (current, desired) {
const fields = desired.state === 'not-configured' ? ['state'] : DEFAULT_SETUP_FIELDS
const modifications = {}
const deletions = {}
for (const field of fields) {
if (desired[field] === undefined) continue
const same = field === 'languages'
? this.sameLanguages(current[field], desired[field])
: current[field] === desired[field]
if (same) continue
deletions[field] = current[field]
const cleared = desired[field] === null || (Array.isArray(desired[field]) && desired[field].length === 0)
if (!cleared) {
modifications[field] = desired[field]
}
}
return { modifications, deletions }
}

sameLanguages (current = [], desired = []) {
const currentSet = new Set(current)
const desiredSet = new Set(desired)
return currentSet.size === desiredSet.size && [...desiredSet].every(lang => currentSet.has(lang))
}

declaredFields (desired) {
const fields = desired.state === 'not-configured' ? ['state'] : DEFAULT_SETUP_FIELDS
return Object.fromEntries(fields.filter(field => desired[field] !== undefined).map(field => [field, desired[field]]))
}

handleError (msg, e) {
const message = `${msg} for ${this.repo.owner}/${this.repo.repo}: ${e.status ? `${e.status} ` : ''}${e.message}`
if (this.nop) {
// As in Diffable: in a dry run the repo may not exist yet.
if (e.status === 404) return []
return [new NopCommand(SECTION, this.repo, null, message, 'ERROR')]
}
this.logError(message)
return []
}
}
7 changes: 6 additions & 1 deletion lib/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2544,8 +2544,12 @@ class Settings {
if (propertyConfig !== undefined) {
for (const source of [sources.org, sources.suborg, sources.repo]) delete source.custom_properties
}
const codeScanningLanguages = Settings.PLUGINS.code_scanning.declaredLanguages(
[sources.org.code_scanning, sources.suborg.code_scanning, sources.repo.code_scanning]
)
const overrideConfig = this.mergeDeep.mergeDeep({}, sources.org, sources.suborg, sources.repo)
if (propertyConfig !== undefined) overrideConfig.custom_properties = propertyConfig
if (codeScanningLanguages !== undefined) overrideConfig.code_scanning.default_setup.languages = codeScanningLanguages

// When processing repos removed from suborg targeting, inject empty arrays
// for plugin sections that were previously provided by the suborg. This
Expand Down Expand Up @@ -3306,7 +3310,8 @@ Settings.PLUGINS = {
custom_properties: require('./plugins/custom_properties.js'),
custom_repository_roles: require('./plugins/custom_repository_roles'),
variables: require('./plugins/variables'),
app_installations: require('./plugins/appInstallations')
app_installations: require('./plugins/appInstallations'),
code_scanning: require('./plugins/code_scanning')
Comment thread
Copilot marked this conversation as resolved.
Resolved
}

module.exports = Settings
Expand Down
148 changes: 148 additions & 0 deletions schema/dereferenced/repos.json
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,80 @@
}
}
}
},
"code_scanning": {
"description": "Code scanning settings",
"type": "object",
"additionalProperties": false,
"properties": {
"default_setup": {
"description": "CodeQL default setup. Only the declared fields are compared and updated; with `state: not-configured`, only the state is applied.",
"type": "object",
"properties": {
"state": {
"description": "The desired state of code scanning default setup.",
"type": "string",
"enum": [
"configured",
"not-configured"
]
},
"runner_type": {
"description": "Runner type to be used.",
"type": "string",
"enum": [
"standard",
"labeled"
]
},
"runner_label": {
"description": "Runner label to be used if the runner type is labeled.",
"type": [
"string",
"null"
],
"examples": [
"code-scanning"
]
},
"query_suite": {
"description": "CodeQL query suite to be used.",
"type": "string",
"enum": [
"default",
"extended"
]
},
"threat_model": {
"description": "Threat model to be used for code scanning analysis. Use `remote` to analyze only network sources and `remote_and_local` to include local sources like filesystem access, command-line arguments, database reads, environment variable and standard input.",
"type": "string",
"enum": [
"remote",
"remote_and_local"
]
},
"languages": {
"description": "CodeQL languages to be analyzed.",
"type": "array",
"items": {
"type": "string",
"enum": [
"actions",
"c-cpp",
"csharp",
"go",
"java-kotlin",
"javascript-typescript",
"python",
"ruby",
"swift"
]
}
}
},
"additionalProperties": false
}
}
}
},
"$defs": {
Expand Down Expand Up @@ -4110,6 +4184,80 @@
}
}
},
"CodeScanningSettings": {
"description": "Code scanning settings",
"type": "object",
"additionalProperties": false,
"properties": {
"default_setup": {
"description": "CodeQL default setup. Only the declared fields are compared and updated; with `state: not-configured`, only the state is applied.",
"type": "object",
"properties": {
"state": {
"description": "The desired state of code scanning default setup.",
"type": "string",
"enum": [
"configured",
"not-configured"
]
},
"runner_type": {
"description": "Runner type to be used.",
"type": "string",
"enum": [
"standard",
"labeled"
]
},
"runner_label": {
"description": "Runner label to be used if the runner type is labeled.",
"type": [
"string",
"null"
],
"examples": [
"code-scanning"
]
},
"query_suite": {
"description": "CodeQL query suite to be used.",
"type": "string",
"enum": [
"default",
"extended"
]
},
"threat_model": {
"description": "Threat model to be used for code scanning analysis. Use `remote` to analyze only network sources and `remote_and_local` to include local sources like filesystem access, command-line arguments, database reads, environment variable and standard input.",
"type": "string",
"enum": [
"remote",
"remote_and_local"
]
},
"languages": {
"description": "CodeQL languages to be analyzed.",
"type": "array",
"items": {
"type": "string",
"enum": [
"actions",
"c-cpp",
"csharp",
"go",
"java-kotlin",
"javascript-typescript",
"python",
"ruby",
"swift"
]
}
}
},
"additionalProperties": false
}
}
},
"VariablesSettings": {
"description": "An Actions variable entry",
"type": "object",
Expand Down
Loading