Skip to content
Merged
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
5 changes: 2 additions & 3 deletions app/lib/build_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ def files_named(name_pattern)
def met_result(result_description, html_url)
{
value: CriterionStatus::MET, confidence: 3,
explanation:
"Non-trivial #{result_description} file in repository: " \
"<#{html_url}>."
explanation: I18n.t('detectives.repo_files.file_found',
description: result_description, url: html_url)
}
end

Expand Down
40 changes: 27 additions & 13 deletions app/lib/chief.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,18 @@ def log_detective_failure(source, e, detective, proposal, data)
end
end

# Execute a block with I18n.locale temporarily set to the project's entry_locale.
# This ensures detective explanations are generated in the project's language.
# @yield Block to execute with project locale set
# @return Result of the block
def with_project_locale
original_locale = I18n.locale
I18n.locale = @entry_locale&.to_sym || :en
yield
ensure
I18n.locale = original_locale
end

# Invoke one "Detective", which will
# analyze the project and reply with an updated changeset in the form
# { fieldname1: { value: value, confidence: 1..5, explanation: text}, ...}
Expand Down Expand Up @@ -264,24 +276,26 @@ def needed_outputs(needed_fields, changed_fields = nil)
# @param changed_fields [Array<Symbol>, nil] Fields that were explicitly changed
# rubocop:disable Metrics/MethodLength
def propose_changes(needed_fields: nil, changed_fields: nil)
current_proposal = {} # Current best changeset.
with_project_locale do
current_proposal = {} # Current best changeset.

# Determine what outputs we need
needed = needed_outputs(needed_fields, changed_fields)
# Determine what outputs we need
needed = needed_outputs(needed_fields, changed_fields)

# Filter to only needed detectives (subset varies per request)
detectives_to_run = filter_needed_detectives(needed)
# Filter to only needed detectives (subset varies per request)
detectives_to_run = filter_needed_detectives(needed)

# Sort that specific subset in dependency order
detectives_to_run = topological_sort_detectives(detectives_to_run)
# Sort that specific subset in dependency order
detectives_to_run = topological_sort_detectives(detectives_to_run)

# Run each detective in the sorted order
detectives_to_run.each do |detective_class|
detective = detective_class.new
detective.octokit_client_factory = @client_factory
current_proposal = propose_one_change(detective, current_proposal)
# Run each detective in the sorted order
detectives_to_run.each do |detective_class|
detective = detective_class.new
detective.octokit_client_factory = @client_factory
current_proposal = propose_one_change(detective, current_proposal)
end
current_proposal
end
current_proposal
end
# rubocop:enable Metrics/MethodLength

Expand Down
15 changes: 7 additions & 8 deletions app/lib/floss_license_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,20 @@ def analyze(_evidence, current)
floss_license_osi_status:
{
value: CriterionStatus::MET, confidence: 5,
explanation: "The #{license} license is approved by the " \
'Open Source Initiative (OSI).'
explanation: I18n.t('detectives.floss_license.osi_approved',
license: license)
},
floss_license_status:
{
value: CriterionStatus::MET, confidence: 5,
explanation: "The #{license} license is approved by the " \
'Open Source Initiative (OSI).'
explanation: I18n.t('detectives.floss_license.osi_approved',
license: license)
},
osps_le_02_01_status:
{
value: CriterionStatus::MET, confidence: 5,
explanation: "The #{license} license for the repository " \
'contents is approved by the ' \
'Open Source Initiative (OSI).'
explanation: I18n.t('detectives.floss_license.osi_approved_repository',
license: license)
},
# We can't know what the license of the *released* assets are
# looking only at the repository, as the LICENSE file is focused
Expand All @@ -176,7 +175,7 @@ def analyze(_evidence, current)
floss_license_osi_status:
{
value: CriterionStatus::UNMET, confidence: 1,
explanation: '// Did not find license in the OSI list.'
explanation: I18n.t('detectives.floss_license.not_in_osi_list')
}
}
else
Expand Down
35 changes: 14 additions & 21 deletions app/lib/github_basic_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,57 +101,50 @@ def analyze(_evidence, current)
# We have a github repo.
results[:repo_public_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'Repository on GitHub, which provides ' \
'public git repositories with URLs.'
explanation: I18n.t('detectives.github.repo_public')
}
results[:repo_track_status] = {
value: CriterionStatus::MET, confidence: 4,
explanation: 'Repository on GitHub, which uses git. ' \
'git can track the changes, ' \
'who made them, and when they were made.'
explanation: I18n.t('detectives.github.repo_track')
}
results[:repo_distributed_status] = {
value: CriterionStatus::MET, confidence: 4,
explanation: 'Repository on GitHub, which uses git. ' \
'git is distributed.'
explanation: I18n.t('detectives.github.repo_distributed')
}
results[:contribution_status] = {
value: CriterionStatus::MET, confidence: 2,
explanation: 'Projects on GitHub by default use issues and ' \
'pull requests, as encouraged by documentation such as ' \
'<https://guides.github.com/activities/' \
'contributing-to-open-source/>.'
explanation: I18n.t('detectives.github.contribution')
}
results[:discussion_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'GitHub supports discussions on issues and pull requests.'
explanation: I18n.t('detectives.github.discussion')
}
# Baseline: public discussion mechanisms (same evidence as discussion_status)
results[:osps_gv_02_01_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'GitHub supports public discussions on proposed changes (via pull requests) and usage obstacles (via issues).'
explanation: I18n.t('detectives.github.osps_gv_02_01')
}
# Baseline criteria - defect reporting instructions (low confidence)
results[:osps_do_02_01_status] = {
value: CriterionStatus::MET, confidence: 2,
explanation: 'GitHub provides defect reporting mechanisms by default (via issues).'
explanation: I18n.t('detectives.github.osps_do_02_01')
}
# 2FA required by GitHub. An organization *might* use multiple repo
# hosts, but given our information, 2FA seems highly likely.
results[:osps_ac_01_01_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'GitHub requires 2FA as of March 2023.'
explanation: I18n.t('detectives.github.osps_ac_01_01')
}
# Publicly readable, if we can read it. It's possible it's not current,
# but if this really is the "main" repo (as claimed) then this is met.
results[:osps_qa_01_01_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'Repository is publicly available on GitHub.'
explanation: I18n.t('detectives.github.osps_qa_01_01')
}
# If the main repo is on GitHub, then git will store this
results[:osps_qa_01_02_status] = {
value: CriterionStatus::MET, confidence: 3,
explanation: 'Repository git metadata is publicly available on GitHub.'
explanation: I18n.t('detectives.github.osps_qa_01_02')
}

# Get basic evidence
Expand All @@ -169,15 +162,15 @@ def analyze(_evidence, current)
if basic_repo_data[:name]
results[:name] = {
value: basic_repo_data[:name],
confidence: 3, explanation: 'GitHub name'
confidence: 3, explanation: I18n.t('detectives.github.name')
}
end
if basic_repo_data[:description]
results[:description] = {
value: basic_repo_data[:description].gsub(
/(\A|\s)\:[a-zA-Z]+\:(\s|\Z)/, ' '
).strip,
confidence: 3, explanation: 'GitHub description'
confidence: 3, explanation: I18n.t('detectives.github.description')
}
end
# rubocop:enable Metrics/BlockLength
Expand All @@ -194,7 +187,7 @@ def analyze(_evidence, current)
license = cleanup_license(license_data_raw[:key])
results[:license] = {
value: license,
confidence: 3, explanation: 'GitHub API license analysis'
confidence: 3, explanation: I18n.t('detectives.github.license')
}
end

Expand All @@ -204,7 +197,7 @@ def analyze(_evidence, current)
results[:implementation_languages] = {
value: implementation_languages,
confidence: 3,
explanation: 'GitHub API implementation language analysis'
explanation: I18n.t('detectives.github.implementation_languages')
}
end

Expand Down
25 changes: 12 additions & 13 deletions app/lib/hardened_sites_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,20 @@ class HardenedSitesDetective < Detective
content-security-policy strict-transport-security
x-content-type-options
].freeze
MET =

def met_result
{
value: CriterionStatus::MET, confidence: 3,
explanation: 'Found all required security hardening headers.'
}.freeze
UNMET_MISSING =
{
value: CriterionStatus::UNMET, confidence: 5,
explanation: 'Required security hardening headers missing: '
}.freeze
UNMET_NOSNIFF =
explanation: I18n.t('detectives.hardened_sites.all_headers_found')
}
end

def unmet_missing_result
{
value: CriterionStatus::UNMET, confidence: 5,
explanation: '// X-Content-Type-Options was not set to "nosniff".'
}.freeze
explanation: I18n.t('detectives.hardened_sites.headers_missing')
}
end

INPUTS = %i[repo_url homepage_url].freeze
OUTPUTS = [:hardened_site_status].freeze
Expand Down Expand Up @@ -114,9 +113,9 @@ def report_on_check_urls(evidence, homepage_url, repo_url)
all_problems = problems_in_urls(evidence, urls)
results[:hardened_site_status] =
if all_problems.empty?
MET
met_result
else
answer = UNMET_MISSING.deep_dup # clone but result is not frozen
answer = unmet_missing_result
answer[:explanation] += all_problems.join(', ')
answer
end
Expand Down
6 changes: 3 additions & 3 deletions app/lib/name_from_url_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ def analyze(_evidence, current)
@results[:name] =
{
value: finding[2], confidence: 1,
explanation: "The project URL's domain name suggests this."
explanation: I18n.t('detectives.name_from_url.domain_suggests')
}
else
finding = name_in_url_tail.match(homepage_url)
if finding
@results[:name] =
{
value: finding[1], confidence: 1,
explanation: "The project URL's tail suggests this."
explanation: I18n.t('detectives.name_from_url.tail_suggests')
}
end
end
Expand All @@ -51,7 +51,7 @@ def analyze(_evidence, current)
@results[:name] =
{
value: finding[1], confidence: 1,
explanation: "The repo URL's tail suggests this."
explanation: I18n.t('detectives.name_from_url.repo_tail_suggests')
}
end
end
Expand Down
9 changes: 5 additions & 4 deletions app/lib/project_sites_https_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ def set_http_results
@results[:sites_https_status] =
{
value: CriterionStatus::UNMET, confidence: 5,
explanation: '// Given an http: URL.'
explanation: I18n.t('detectives.project_sites_https.given_http')
}
# Any official channel using http is a problem
@results[:osps_br_03_01_status] =
{
value: CriterionStatus::UNMET, confidence: 5,
explanation: 'Project URLs lists http (not https) as official.'
explanation: I18n.t('detectives.project_sites_https.official_http')
}
# We don't know enough to be *certain* what the distribution channels
# are; it's possible that the official distribution channels *are*
Expand All @@ -59,7 +59,7 @@ def set_http_results
@results[:osps_br_03_02_status] =
{
value: CriterionStatus::UNMET, confidence: 3,
explanation: 'We were given a URL that uses http (not https).'
explanation: I18n.t('detectives.project_sites_https.url_uses_http')
}
end
# rubocop:enable Metrics/MethodLength
Expand All @@ -69,7 +69,8 @@ def met_result(explanation)
end

def set_https_results
@results[:sites_https_status] = met_result('Given only https: URLs.')
@results[:sites_https_status] =
met_result(I18n.t('detectives.project_sites_https.given_https'))
@results[:osps_br_03_01_status] =
met_result('Project URLs use HTTPS exclusively.')
@results[:osps_br_03_02_status] =
Expand Down
17 changes: 8 additions & 9 deletions app/lib/repo_files_examine_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,16 @@ def directory_named(name_pattern)
def unmet_result(result_description, confidence: 1)
{
value: CriterionStatus::UNMET, confidence: confidence,
explanation: "// No #{result_description} file found."
explanation: I18n.t('detectives.repo_files.no_file_found',
description: result_description)
}
end

def met_result(result_description, html_url, confidence: 3)
{
value: CriterionStatus::MET, confidence: confidence,
explanation:
"Non-trivial #{result_description} file in repository: " \
"<#{html_url}>."
explanation: I18n.t('detectives.repo_files.file_found',
description: result_description, url: html_url)
}
end

Expand Down Expand Up @@ -135,7 +135,7 @@ def set_baseline_contribution_status
confidence = @results[:contribution_status][:confidence]
@results[:osps_gv_03_01_status] = {
value: CriterionStatus::MET, confidence: confidence,
explanation: 'Contribution process documented in repository.'
explanation: I18n.t('detectives.repo_files.contribution_documented')
}
end

Expand Down Expand Up @@ -164,17 +164,16 @@ def set_baseline_license_status
confidence = @results[:license_location_status][:confidence]
@results[:osps_le_03_01_status] = {
value: CriterionStatus::MET, confidence: confidence,
explanation: 'License file found in repository.'
explanation: I18n.t('detectives.repo_files.license_found')
}
else
@results[:osps_le_03_01_status] = {
value: CriterionStatus::UNMET, confidence: 5,
explanation: 'License file not found in repository.'
explanation: I18n.t('detectives.repo_files.license_not_found')
}
@results[:osps_le_03_02_status] = {
value: CriterionStatus::UNMET, confidence: 2,
explanation: 'License file not found in repository (likely not ' \
'included in releases).'
explanation: I18n.t('detectives.repo_files.license_not_in_releases')
}
end
end
Expand Down
10 changes: 6 additions & 4 deletions app/lib/subdir_file_contents_detective.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,24 @@ class SubdirFileContentsDetective < Detective
def unmet_result(result_description)
{
value: CriterionStatus::UNMET, confidence: 1,
explanation: "// No #{result_description} file(s) found."
explanation: I18n.t('detectives.subdir_files.no_files_found',
description: result_description)
}
end

def unmet_result_folder(result_description)
{
value: CriterionStatus::UNMET, confidence: 3,
explanation: "// No appropriate folder found for #{result_description}."
explanation: I18n.t('detectives.subdir_files.no_folder_found',
description: result_description)
}
end

def met_result(result_description)
{
value: CriterionStatus::MET, confidence: 3,
explanation:
"Some #{result_description} file contents found."
explanation: I18n.t('detectives.subdir_files.some_contents_found',
description: result_description)
}
end

Expand Down
Loading