#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# TeamFile audit CGI
#------------------------------------------------------------------------------

use strict;
use utf8;
use CGI;
use HTML::Template;
use JSON::PP qw(decode_json);
use TF::CommonUtils;

binmode(STDOUT, ":utf8");

my $AUDIT_ROOT    = '/var/lib/teamfile/audit';
my $BRAND         = $ENV{'TF_BRANDNAME'} || '';
my $LOCATION      = $ENV{'TF_LOCATION'} || '';
my $USERTYPE      = $ENV{'TF_USERTYPE'} || 'normal';
my $LANGPARAM     = TF::CommonUtils::getLangPriority();
my $TEMPLATE_FILE = './tmpl/audit.tmpl.' . $LANGPARAM;
my %ISSUE_DELTAS  = (
	critical => 15,
	warn => 8,
	caution => 3,
	ok => 0,
);
my %CATEGORY_LIMITS = (
	user => 40,
	file => 35,
	oplog => 25,
);

my $ERRORTABLE_ja = {
	CGIError => 'CGIエラーです',
};
my $ERRORTABLE_en = {
	CGIError => 'CGI error found.',
};
my $ERRORTABLE = ($LANGPARAM eq 'ja') ? $ERRORTABLE_ja : $ERRORTABLE_en;

if (!$LOCATION || !$USERTYPE || $USERTYPE ne 'admin') {
	TF::CommonUtils::showAccessDeny();
	die '';
}

main();

sub main {
	my $cgi = CGI->new;
	if (!$cgi) {
		TF::CommonUtils::showErrorPage($ERRORTABLE->{CGIError}, $LANGPARAM);
		return;
	}
	if ($cgi->cgi_error) {
		TF::CommonUtils::showErrorPage($ERRORTABLE->{CGIError} . '(' . $cgi->cgi_error . ')', $LANGPARAM);
		return;
	}

	my %params = %{base_params()};
	my $user_audit_ref = read_latest_audit($LOCATION, 'user');
	if ($user_audit_ref) {
		%params = (%params, %{build_user_audit_params($user_audit_ref)});
	}
	else {
		$params{userAuditRun} = 0;
		$params{hasUserAuditResult} = 0;
	}
	my $file_audit_ref = read_latest_audit($LOCATION, 'file');
	if ($file_audit_ref) {
		%params = (%params, %{build_file_audit_params($file_audit_ref)});
	}
	else {
		$params{fileAuditRun} = 0;
		$params{hasFileAuditResult} = 0;
	}
	my $oplog_audit_ref = read_latest_audit($LOCATION, 'oplog');
	if ($oplog_audit_ref) {
		%params = (%params, %{build_oplog_audit_params($oplog_audit_ref)});
	}
	else {
		$params{oplogAuditRun} = 0;
		$params{hasOplogAuditResult} = 0;
	}
	%params = (%params, %{build_overall_params($user_audit_ref, $file_audit_ref, $oplog_audit_ref)});

	print_response($cgi, \%params);
}

sub read_latest_audit {
	my ($location, $type) = @_;
	$location =~ s/^\/*//;
	$location =~ s/\/*$//;
	$location =~ s/[^A-Za-z0-9_-]/_/g;
	return undef if !$location;
	return undef if !$type || $type !~ /^(user|file|oplog)$/;

	my $path = "$AUDIT_ROOT/$location/latest-$type-audit.json";
	return undef if !-f $path;

	if (open(my $fh, '<', $path)) {
		binmode($fh);
		local $/ = undef;
		my $json = <$fh>;
		close($fh);
		return eval { decode_json($json) };
	}
	return undef;
}

sub build_user_audit_params {
	my $audit_ref = shift || {};
	my %summary = %{$audit_ref->{summary} || {}};
	my %details = %{$audit_ref->{details} || {}};

	my @issues = map { issue_row($_) } @{$audit_ref->{issues} || []};
	my @unused_users = map { user_row($_) } @{$details{unused_users} || []};
	my @allowhosts_missing_users = map { user_row($_) } @{$details{allowhosts_missing_users} || []};
	my @admin_allowhosts_missing_users = map { user_row($_) } @{$details{admin_allowhosts_missing_users} || []};

	return {
		userAuditRun => 1,
		hasUserAuditResult => 1,
		generatedAt => h(format_dt($audit_ref->{generated_at} || '')),
		targetDate => h($audit_ref->{target_date} || ''),
		userElapsed => h(format_elapsed($audit_ref->{elapsed_ms} || 0)),
		auditStatus => h($audit_ref->{status} || ''),
		totalCount => $summary{total_users} || 0,
		activeCount => $summary{active_users} || 0,
		allowhostsCount => $summary{allowhosts_users} || 0,
		allowhostsRate => $summary{allowhosts_rate} || 0,
		unusedCount => $summary{unused_users} || 0,
		allowhostsMissingCount => $summary{allowhosts_missing_users} || 0,
		adminAllowhostsMissingCount => $summary{admin_allowhosts_missing_users} || 0,
		issues => \@issues,
		unusedUsers => \@unused_users,
		allowhostsMissingUsers => \@allowhosts_missing_users,
		adminAllowhostsMissingUsers => \@admin_allowhosts_missing_users,
		hasUnusedUsers => scalar(@unused_users) > 0 ? 1 : 0,
		hasAllowhostsMissingUsers => scalar(@allowhosts_missing_users) > 0 ? 1 : 0,
		hasAdminAllowhostsMissingUsers => scalar(@admin_allowhosts_missing_users) > 0 ? 1 : 0,
	};
}

sub build_file_audit_params {
	my $audit_ref = shift || {};
	my %summary = %{$audit_ref->{summary} || {}};
	my %details = %{$audit_ref->{details} || {}};

	my @issues = map { issue_row($_) } @{$audit_ref->{issues} || []};
	my @top_users = map { file_user_quota_row($_) } @{$details{top_users} || []};
	my @inactive_users = map { inactive_user_quota_row($_) } @{$details{inactive_users} || []};
	my @expired_boxes = map { box_row($_) } @{$details{expired_boxes} || []};
	my @unlimited_boxes = map { box_row($_) } @{$details{unlimited_boxes} || []};
	my @long_boxes = map { box_row($_) } @{$details{long_boxes} || []};
	my @large_files = map { file_row($_) } @{$details{large_files} || []};
	my @stale_files = map { file_row($_) } @{$details{stale_files} || []};

	return {
		fileAuditRun => 1,
		hasFileAuditResult => 1,
		fileGeneratedAt => h(format_dt($audit_ref->{generated_at} || '')),
		fileTargetDate => h($audit_ref->{target_date} || ''),
		fileElapsed => h(format_elapsed($audit_ref->{elapsed_ms} || 0)),
		fileAuditStatus => h($audit_ref->{status} || ''),
		fileDiskRate => $summary{disk_size_rate} || 0,
		fileDiskUsed => h(format_bytes($summary{disk_used_bytes} || 0)),
		fileDiskMax => h(($summary{disk_max_bytes} || 0) > 0 ? format_bytes($summary{disk_max_bytes}) : localize('unlimited')),
		fileDiskUsedFiles => $summary{disk_used_files} || 0,
		fileBoxOpenCount => $summary{box_open} || 0,
		fileBoxExpiredOpenCount => $summary{box_expired_open} || 0,
		fileBoxUnlimitedOpenCount => $summary{box_unlimited_open} || 0,
		fileBoxLongOpenCount => $summary{box_long_open} || 0,
		fileLargeCount => $summary{large_files} || 0,
		fileHugeCount => $summary{huge_files} || 0,
		fileStaleCount => $summary{stale_files} || 0,
		fileVeryStaleCount => $summary{very_stale_files} || 0,
		fileIssues => \@issues,
		fileTopUsers => \@top_users,
		fileInactiveUsers => \@inactive_users,
		fileExpiredBoxes => \@expired_boxes,
		fileUnlimitedBoxes => \@unlimited_boxes,
		fileLongBoxes => \@long_boxes,
		fileLargeFiles => \@large_files,
		fileStaleFiles => \@stale_files,
		hasFileTopUsers => scalar(@top_users) > 0 ? 1 : 0,
		hasFileInactiveUsers => scalar(@inactive_users) > 0 ? 1 : 0,
		hasFileExpiredBoxes => scalar(@expired_boxes) > 0 ? 1 : 0,
		hasFileUnlimitedBoxes => scalar(@unlimited_boxes) > 0 ? 1 : 0,
		hasFileLongBoxes => scalar(@long_boxes) > 0 ? 1 : 0,
		hasFileLargeFiles => scalar(@large_files) > 0 ? 1 : 0,
		hasFileStaleFiles => scalar(@stale_files) > 0 ? 1 : 0,
	};
}

sub build_oplog_audit_params {
	my $audit_ref = shift || {};
	my %summary = %{$audit_ref->{summary} || {}};
	my %details = %{$audit_ref->{details} || {}};

	my @issues = map { issue_row($_) } @{$audit_ref->{issues} || []};
	my @period_ip_drift = map { oplog_ip_row($_) } @{$details{period_ip_drift} || []};
	my @daily_ip_drift = map { oplog_ip_row($_) } @{$details{daily_ip_drift} || []};
	my @bulk_put = map { oplog_count_row($_) } @{$details{bulk_put} || []};
	my @bulk_delete = map { oplog_count_row($_) } @{$details{bulk_delete} || []};
	my @offhours = map { oplog_count_row($_) } @{$details{offhours} || []};
	my @failed_status = map { oplog_count_row($_) } @{$details{failed_status} || []};
	my @auth_failed_users = map { oplog_auth_user_row($_) } @{$details{auth_failed_users} || []};
	my @auth_failed_ips = map { oplog_auth_ip_row($_) } @{$details{auth_failed_ips} || []};
	my @auth_lockouts = map { oplog_auth_user_row($_) } @{$details{auth_lockouts} || []};
	my @bulk_get = map { oplog_security_row($_) } @{$details{bulk_get} || []};
	my @denied_users = map { oplog_denied_row($_) } @{$details{denied_users} || []};
	my @denied_ips = map { oplog_denied_row($_) } @{$details{denied_ips} || []};
	my @webdav_methods = map { oplog_security_row($_) } @{$details{webdav_methods} || []};
	my @management_changes = map { oplog_security_row($_) } @{$details{management_changes} || []};
	my @missing_indexes = map { { date => h($_->{date} || '-') } } @{$details{missing_indexes} || []};

	return {
		oplogAuditRun => 1,
		hasOplogAuditResult => 1,
		oplogGeneratedAt => h(format_dt($audit_ref->{generated_at} || '')),
		oplogTargetDate => h($audit_ref->{target_date} || ''),
		oplogPeriodDays => $audit_ref->{period_days} || 0,
		oplogPeriodIpDriftDesc => h(sprintf(localize('oplogPeriodIpDriftDesc'), $audit_ref->{period_days} || 0)),
		oplogOffhoursDesc => h(localize('oplogOffhoursDesc')),
		oplogElapsed => h(format_elapsed($audit_ref->{elapsed_ms} || 0)),
		oplogAuditStatus => h($audit_ref->{status} || ''),
		oplogIndexedDays => $summary{indexed_days} || 0,
		oplogTotalRows => $summary{total_rows} || 0,
		oplogGroupCount => $summary{group_count} || 0,
		oplogIpDriftCount => ($summary{daily_ip_drift_users} || 0) + ($summary{period_ip_drift_warn_users} || 0) + ($summary{period_ip_drift_caution_users} || 0),
		oplogBulkPutCount => ($summary{bulk_put_warn_groups} || 0) + ($summary{bulk_put_caution_groups} || 0),
		oplogBulkDeleteCount => ($summary{bulk_delete_critical_groups} || 0) + ($summary{bulk_delete_warn_groups} || 0),
		oplogOffhoursCount => $summary{offhours_groups} || 0,
		oplogFailedStatusCount => $summary{failed_status_groups} || 0,
		oplogAuthFailedCount => $summary{auth_failed_events} || 0,
		oplogAuthFailedCountedCount => $summary{auth_failed_counted_events} || 0,
		oplogAuthLockedCount => $summary{auth_locked_users} || 0,
		oplogBulkGetCount => ($summary{bulk_get_critical_users} || 0) + ($summary{bulk_get_warn_users} || 0),
		oplogDeniedCount => ($summary{denied_users} || 0) + ($summary{denied_ips} || 0),
		oplogWebdavMethodCount => $summary{webdav_method_users} || 0,
		oplogManagementChangeCount => $summary{management_change_users} || 0,
		oplogMissingIndexCount => $summary{missing_index_days} || 0,
		oplogIssues => \@issues,
		oplogPeriodIpDrift => \@period_ip_drift,
		oplogDailyIpDrift => \@daily_ip_drift,
		oplogBulkPut => \@bulk_put,
		oplogBulkDelete => \@bulk_delete,
		oplogOffhours => \@offhours,
		oplogFailedStatus => \@failed_status,
		oplogAuthFailedUsers => \@auth_failed_users,
		oplogAuthFailedIps => \@auth_failed_ips,
		oplogAuthLockouts => \@auth_lockouts,
		oplogBulkGet => \@bulk_get,
		oplogDeniedUsers => \@denied_users,
		oplogDeniedIps => \@denied_ips,
		oplogWebdavMethods => \@webdav_methods,
		oplogManagementChanges => \@management_changes,
		oplogMissingIndexes => \@missing_indexes,
		hasOplogPeriodIpDrift => scalar(@period_ip_drift) > 0 ? 1 : 0,
		hasOplogDailyIpDrift => scalar(@daily_ip_drift) > 0 ? 1 : 0,
		hasOplogBulkPut => scalar(@bulk_put) > 0 ? 1 : 0,
		hasOplogBulkDelete => scalar(@bulk_delete) > 0 ? 1 : 0,
		hasOplogOffhours => scalar(@offhours) > 0 ? 1 : 0,
		hasOplogFailedStatus => scalar(@failed_status) > 0 ? 1 : 0,
		hasOplogAuthFailedUsers => scalar(@auth_failed_users) > 0 ? 1 : 0,
		hasOplogAuthFailedIps => scalar(@auth_failed_ips) > 0 ? 1 : 0,
		hasOplogAuthLockouts => scalar(@auth_lockouts) > 0 ? 1 : 0,
		hasOplogBulkGet => scalar(@bulk_get) > 0 ? 1 : 0,
		hasOplogDeniedUsers => scalar(@denied_users) > 0 ? 1 : 0,
		hasOplogDeniedIps => scalar(@denied_ips) > 0 ? 1 : 0,
		hasOplogWebdavMethods => scalar(@webdav_methods) > 0 ? 1 : 0,
		hasOplogManagementChanges => scalar(@management_changes) > 0 ? 1 : 0,
		hasOplogMissingIndexes => scalar(@missing_indexes) > 0 ? 1 : 0,
	};
}

sub build_overall_params {
	my ($user_audit_ref, $file_audit_ref, $oplog_audit_ref) = @_;
	my %counts = (critical => 0, warn => 0, caution => 0);
	my %deductions = (
		user => audit_deduction($user_audit_ref, $CATEGORY_LIMITS{user}),
		file => audit_deduction($file_audit_ref, $CATEGORY_LIMITS{file}),
		oplog => audit_deduction($oplog_audit_ref, $CATEGORY_LIMITS{oplog}),
	);

	for my $audit_ref (grep { $_ } ($user_audit_ref, $file_audit_ref, $oplog_audit_ref)) {
		for my $issue_ref (@{$audit_ref->{issues} || []}) {
			my $level = $issue_ref->{level} || '';
			$counts{$level}++ if exists $counts{$level};
		}
	}

	my $total_deduction = $deductions{user} + $deductions{file} + $deductions{oplog};
	my $score = 100 - $total_deduction;
	$score = 0 if $score < 0;
	my $rank = 'good';
	if ($score < 60) {
		$rank = 'bad';
	}
	elsif ($score < 80 || $counts{critical} > 0 || $counts{warn} > 0) {
		$rank = 'watch';
	}

	return {
		overallScore => $score,
		overallRank => h($rank),
		overallRankLabel => h(localize('rank_' . $rank)),
		overallTitle => h(localize('overallTitle')),
		overallDesc => h(localize('overallDesc')),
		overallFormula => h(score_formula($score, \%deductions, \%counts)),
		overallScoreLabel => h(localize('overallScoreLabel')),
		overallCriticalLabel => h(localize('level_critical')),
		overallWarnLabel => h(localize('level_warn')),
		overallCautionLabel => h(localize('level_caution')),
		overallCriticalCount => $counts{critical},
		overallWarnCount => $counts{warn},
		overallCautionCount => $counts{caution},
		userAuditDeduction => h(format_deduction($deductions{user}, $CATEGORY_LIMITS{user})),
		fileAuditDeduction => h(format_deduction($deductions{file}, $CATEGORY_LIMITS{file})),
		oplogAuditDeduction => h(format_deduction($deductions{oplog}, $CATEGORY_LIMITS{oplog})),
		tabOverviewLabel => h(localize('tabOverviewLabel')),
		tabUserLabel => h(localize('tabUserLabel')),
		tabFileLabel => h(localize('tabFileLabel')),
		tabOplogLabel => h(localize('tabOplogLabel')),
	};
}

sub audit_deduction {
	my ($audit_ref, $limit) = @_;
	return 0 if !$audit_ref;

	my $deduction = 0;
	for my $issue_ref (@{$audit_ref->{issues} || []}) {
		my $level = $issue_ref->{level} || '';
		$deduction += $ISSUE_DELTAS{$level} || 0;
	}
	return $deduction > $limit ? $limit : $deduction;
}

sub format_deduction {
	my ($deduction, $limit) = @_;
	my $current = ($deduction || 0) > 0 ? '-' . $deduction : '0';
	return $current . ' / -' . ($limit || 0);
}

sub score_formula {
	my ($score, $deductions_ref, $counts_ref) = @_;
	my %deductions = %{$deductions_ref || {}};
	my %counts = %{$counts_ref || {}};
	if ($LANGPARAM eq 'ja') {
		return '100点 - ユーザ監査 ' . ($deductions{user} || 0) . '点 - ファイル監査 ' . ($deductions{file} || 0) . '点 - 操作ログ監査 ' . ($deductions{oplog} || 0) . '点 = ' . $score . '点'
			. '（重要 ' . ($counts{critical} || 0) . '件 x 15点、警告 ' . ($counts{warn} || 0) . '件 x 8点、注意 ' . ($counts{caution} || 0) . '件 x 3点。カテゴリ上限あり）';
	}
	return '100 - User audit ' . ($deductions{user} || 0) . ' - File audit ' . ($deductions{file} || 0) . ' - Operation log audit ' . ($deductions{oplog} || 0) . ' = ' . $score
		. ' (Critical ' . ($counts{critical} || 0) . ' x 15, Warning ' . ($counts{warn} || 0) . ' x 8, Caution ' . ($counts{caution} || 0) . ' x 3. Category caps apply.)';
}

sub user_row {
	my $user_ref = shift || {};
	my $unused_days = $user_ref->{unused_days};

	return {
		userid => h($user_ref->{userid} || ''),
		name => h($user_ref->{name} || ''),
		mailaddr => h($user_ref->{mailaddr} || ''),
		usertype => h(localize_user_type($user_ref->{usertype} || '')),
		state => h(localize($user_ref->{state} || '')),
		reason => h(localize_reason($user_ref->{reason} || '', $unused_days)),
		lastaccessdt => h(format_dt($user_ref->{lastaccessdt} || '') || '-'),
		lastaccessclient => h($user_ref->{lastaccessclient} || '-'),
		allowhosts => h($user_ref->{allowhosts} || '-'),
	};
}

sub issue_row {
	my $issue_ref = shift || {};
	my $level = $issue_ref->{level} || '';
	my $code = $issue_ref->{code} || '';

	return {
		level => h($level),
		levelLabel => h(localize('level_' . $level)),
		title => h(localize('issue_' . $code . '_title')),
		body => h(localize('issue_' . $code . '_body')),
		scoreDelta => h(issue_score_delta($level)),
	};
}

sub issue_score_delta {
	my $level = shift || '';
	my $value = $ISSUE_DELTAS{$level} || 0;
	return $value > 0 ? "-$value" : "0";
}

sub file_user_quota_row {
	my $row_ref = shift || {};
	return {
		userid => h($row_ref->{userid} || ''),
		name => h($row_ref->{name} || ''),
		usedSize => h($row_ref->{used_size} || ''),
		usedFiles => $row_ref->{used_files} || 0,
		sizeRate => $row_ref->{size_rate} || 0,
	};
}

sub inactive_user_quota_row {
	my $row_ref = shift || {};
	return {
		userid => h($row_ref->{userid} || ''),
		name => h($row_ref->{name} || ''),
		usedSize => h($row_ref->{used_size} || ''),
		usedFiles => $row_ref->{used_files} || 0,
	};
}

sub box_row {
	my $row_ref = shift || {};
	return {
		name => h($row_ref->{name} || ''),
		groupName => h($row_ref->{group_name} || '-'),
		uri => h($row_ref->{uri} || ''),
		creator => h($row_ref->{creator_display} || $row_ref->{creator} || ''),
		createDt => h(format_dt($row_ref->{create_dt} || '') || '-'),
		expirationDt => h(format_dt($row_ref->{expiration_dt} || '') || localize('unlimited')),
		viewcount => $row_ref->{viewcount} || 0,
		tomailaddr => h($row_ref->{tomailaddr} || '-'),
		message => h($row_ref->{message} || '-'),
	};
}

sub file_row {
	my $row_ref = shift || {};
	my $stale_days = $row_ref->{stale_days};
	return {
		uri => h($row_ref->{uri} || ''),
		size => h($row_ref->{size} || ''),
		lastmodifiedDt => h(format_dt($row_ref->{lastmodified_dt} || '') || '-'),
		staleDays => defined($stale_days) ? $stale_days : '-',
		lastmodifier => h($row_ref->{lastmodifier} || ''),
	};
}

sub oplog_ip_row {
	my $row_ref = shift || {};
	return {
		date => h($row_ref->{date} || '-'),
		user => h($row_ref->{user} || ''),
		ipCount => $row_ref->{ip_count} || 0,
		operationCount => $row_ref->{operation_count} || 0,
		ips => h($row_ref->{ips} || ''),
	};
}

sub oplog_count_row {
	my $row_ref = shift || {};
	return {
		date => h($row_ref->{date} || '-'),
		group => h($row_ref->{group} || ''),
		count => $row_ref->{count} || 0,
		level => h($row_ref->{level} || ''),
	};
}

sub oplog_auth_user_row {
	my $row_ref = shift || {};
	return {
		user => h($row_ref->{user} || ''),
		name => h($row_ref->{name} || ''),
		mailaddr => h($row_ref->{mailaddr} || ''),
		currentFailedCount => $row_ref->{current_failed_count} || 0,
		activeStatus => defined($row_ref->{active}) && $row_ref->{active} ne '' ? ($row_ref->{active} ? localize('active') : localize('inactive')) : '',
		count => $row_ref->{count} || 0,
		countedCount => $row_ref->{counted_count} || 0,
		lockedCount => $row_ref->{locked_count} || 0,
		ipCount => $row_ref->{ip_count} || 0,
		ips => h($row_ref->{ips} || ''),
		firstTime => h($row_ref->{first_time} || ''),
		lastTime => h($row_ref->{last_time} || ''),
		level => h($row_ref->{level} || ''),
	};
}

sub oplog_auth_ip_row {
	my $row_ref = shift || {};
	return {
		ip => h($row_ref->{ip} || ''),
		userCount => $row_ref->{user_count} || 0,
		count => $row_ref->{count} || 0,
		countedCount => $row_ref->{counted_count} || 0,
		lockedCount => $row_ref->{locked_count} || 0,
		users => h($row_ref->{users} || ''),
		firstTime => h($row_ref->{first_time} || ''),
		lastTime => h($row_ref->{last_time} || ''),
		level => h($row_ref->{level} || ''),
	};
}

sub oplog_security_row {
	my $row_ref = shift || {};
	return {
		date => h($row_ref->{date} || '-'),
		user => h($row_ref->{user} || ''),
		count => $row_ref->{count} || 0,
		size => h(format_bytes($row_ref->{bytes} || 0)),
		methods => h($row_ref->{methods} || ''),
		level => h($row_ref->{level} || ''),
	};
}

sub oplog_denied_row {
	my $row_ref = shift || {};
	return {
		subject => h($row_ref->{subject} || ''),
		count => $row_ref->{count} || 0,
		peerCount => $row_ref->{peer_count} || 0,
		targetCount => $row_ref->{target_count} || 0,
		targets => h($row_ref->{targets} || ''),
	};
}

sub base_params {
	return {
		brandname => $BRAND,
		title => localize('title'),
		subtitle => localize('subtitle'),
		userAuditTitle => localize('userAuditTitle'),
		userAuditDesc => localize('userAuditDesc'),
		fileAuditTitle => localize('fileAuditTitle'),
		fileAuditDesc => localize('fileAuditDesc'),
		oplogAuditTitle => localize('oplogAuditTitle'),
		oplogAuditDesc => localize('oplogAuditDesc'),
		auditDeductionLabel => localize('auditDeductionLabel'),
		showLatestLabel => localize('showLatestLabel'),
		preparingLabel => localize('preparingLabel'),
		noLatestResult => localize('noLatestResult'),
		latestMetaTitle => localize('latestMetaTitle'),
		generatedAtLabel => localize('generatedAtLabel'),
		targetDateLabel => localize('targetDateLabel'),
		statusLabel => localize('statusLabel'),
		elapsedLabel => localize('elapsedLabel'),
		summaryTotal => localize('summaryTotal'),
		summaryActive => localize('summaryActive'),
		summaryAllowhostsRate => localize('summaryAllowhostsRate'),
		summaryUnused => localize('summaryUnused'),
		summaryAllowhostsMissing => localize('summaryAllowhostsMissing'),
		summaryAdminAllowhostsMissing => localize('summaryAdminAllowhostsMissing'),
		sectionIssues => localize('sectionIssues'),
		sectionAdminAllowhosts => localize('sectionAdminAllowhosts'),
		sectionAllowhosts => localize('sectionAllowhosts'),
		sectionUnused => localize('sectionUnused'),
		sectionUnusedDesc => localize('sectionUnusedDesc'),
		colUserId => localize('colUserId'),
		colName => localize('colName'),
		colMail => localize('colMail'),
		colType => localize('colType'),
		colState => localize('colState'),
		colReason => localize('colReason'),
		colLastAccess => localize('colLastAccess'),
		colLastClient => localize('colLastClient'),
		colAllowhosts => localize('colAllowhosts'),
		noRows => localize('noRows'),
		overviewHeading => localize('overviewHeading'),
		overviewUserNote => localize('overviewUserNote'),
		overviewFileNote => localize('overviewFileNote'),
		fileLatestMetaTitle => localize('fileLatestMetaTitle'),
		fileSummaryDiskUsage => localize('fileSummaryDiskUsage'),
		fileSummaryDiskUsed => localize('fileSummaryDiskUsed'),
		fileSummaryBoxOpen => localize('fileSummaryBoxOpen'),
		fileSummaryBoxRisk => localize('fileSummaryBoxRisk'),
		fileSummaryLarge => localize('fileSummaryLarge'),
		fileSummaryStale => localize('fileSummaryStale'),
		sectionFileIssues => localize('sectionFileIssues'),
		sectionFileTopUsers => localize('sectionFileTopUsers'),
		sectionFileInactiveUsers => localize('sectionFileInactiveUsers'),
		sectionFileBoxExpired => localize('sectionFileBoxExpired'),
		sectionFileBoxUnlimited => localize('sectionFileBoxUnlimited'),
		sectionFileBoxLong => localize('sectionFileBoxLong'),
		sectionFileLarge => localize('sectionFileLarge'),
		sectionFileStale => localize('sectionFileStale'),
		colUsedSize => localize('colUsedSize'),
		colUsedFiles => localize('colUsedFiles'),
		colRate => localize('colRate'),
		colUri => localize('colUri'),
		colBoxName => localize('colBoxName'),
		colGroupName => localize('colGroupName'),
		colToMail => localize('colToMail'),
		colMessage => localize('colMessage'),
		colCreator => localize('colCreator'),
		colCreated => localize('colCreated'),
		colExpiration => localize('colExpiration'),
		colViewcount => localize('colViewcount'),
		colSize => localize('colSize'),
		colLastModified => localize('colLastModified'),
		colStaleDays => localize('colStaleDays'),
		colLastModifier => localize('colLastModifier'),
		oplogLatestMetaTitle => localize('oplogLatestMetaTitle'),
		oplogSummaryIndexedDays => localize('oplogSummaryIndexedDays'),
		oplogSummaryRows => localize('oplogSummaryRows'),
		oplogSummaryGroups => localize('oplogSummaryGroups'),
		oplogSummaryIpDrift => localize('oplogSummaryIpDrift'),
		oplogSummaryBulkPut => localize('oplogSummaryBulkPut'),
		oplogSummaryBulkDelete => localize('oplogSummaryBulkDelete'),
		oplogSummaryAuthFailed => localize('oplogSummaryAuthFailed'),
		oplogSummaryAuthLocked => localize('oplogSummaryAuthLocked'),
		oplogSummaryBulkGet => localize('oplogSummaryBulkGet'),
		oplogSummaryDenied => localize('oplogSummaryDenied'),
		oplogSummaryWebdavMethods => localize('oplogSummaryWebdavMethods'),
		oplogSummaryManagementChanges => localize('oplogSummaryManagementChanges'),
		oplogSummaryMissingIndexes => localize('oplogSummaryMissingIndexes'),
		sectionOplogIssues => localize('sectionOplogIssues'),
		sectionOplogAuthFailedUsers => localize('sectionOplogAuthFailedUsers'),
		sectionOplogAuthFailedIps => localize('sectionOplogAuthFailedIps'),
		sectionOplogAuthLockouts => localize('sectionOplogAuthLockouts'),
		sectionOplogDailyIpDrift => localize('sectionOplogDailyIpDrift'),
		sectionOplogPeriodIpDrift => localize('sectionOplogPeriodIpDrift'),
		sectionOplogBulkPut => localize('sectionOplogBulkPut'),
		sectionOplogBulkDelete => localize('sectionOplogBulkDelete'),
		sectionOplogOffhours => localize('sectionOplogOffhours'),
		sectionOplogFailedStatus => localize('sectionOplogFailedStatus'),
		sectionOplogBulkGet => localize('sectionOplogBulkGet'),
		sectionOplogDeniedUsers => localize('sectionOplogDeniedUsers'),
		sectionOplogDeniedIps => localize('sectionOplogDeniedIps'),
		sectionOplogWebdavMethods => localize('sectionOplogWebdavMethods'),
		sectionOplogManagementChanges => localize('sectionOplogManagementChanges'),
		sectionOplogMissingIndexes => localize('sectionOplogMissingIndexes'),
		colDate => localize('colDate'),
		colGroup => localize('colGroup'),
		colCount => localize('colCount'),
		colIpCount => localize('colIpCount'),
		colOperationCount => localize('colOperationCount'),
		colIps => localize('colIps'),
		colCountedCount => localize('colCountedCount'),
		colLockedCount => localize('colLockedCount'),
		colUserCount => localize('colUserCount'),
		colFirstTime => localize('colFirstTime'),
		colLastTime => localize('colLastTime'),
		colUsers => localize('colUsers'),
		colCurrentFailedCount => localize('colCurrentFailedCount'),
		colActiveStatus => localize('colActiveStatus'),
		colPeriodDays => localize('colPeriodDays'),
		colSubject => localize('colSubject'),
		colPeerCount => localize('colPeerCount'),
		colTargetCount => localize('colTargetCount'),
		colTargets => localize('colTargets'),
		colMethods => localize('colMethods'),
	};
}

sub print_response {
	my ($cgi, $params_ref) = @_;

	if (open(my $tmplfh, "<", $TEMPLATE_FILE)) {
		binmode($tmplfh, ":utf8");
		my $htmltmpl = HTML::Template->new(
			filehandle => $tmplfh,
			case_sensitive => 1,
			die_on_bad_params => 0,
		);
		$htmltmpl->param(%{$params_ref || {}});
		print $cgi->header(-charset => 'utf-8');
		print $htmltmpl->output;
		close($tmplfh);
	}
	else {
		TF::CommonUtils::showTemplateOpenErr($LANGPARAM);
	}
}

sub h {
	my $str = shift;
	my $escaped = TF::CommonUtils::escapeHTML(defined($str) ? $str : '', 0);
	return $escaped;
}

sub format_dt {
	my $dt = shift || '';
	$dt =~ s/T/ /;
	return $dt;
}

sub format_elapsed {
	my $elapsed_ms = shift || 0;
	return sprintf('%.3f sec', $elapsed_ms / 1000);
}

sub format_bytes {
	my $bytes = shift || 0;
	my @units = ('B', 'KB', 'MB', 'GB', 'TB');
	my $value = $bytes;
	my $unit = shift @units;
	while ($value >= 1024 && @units) {
		$value = $value / 1024;
		$unit = shift @units;
	}
	return ($unit eq 'B') ? sprintf('%d %s', $value, $unit) : sprintf('%.1f %s', $value, $unit);
}

sub localize_user_type {
	my $type = shift || '';
	return $type if $LANGPARAM ne 'ja';
	my %labels = (
		admin => '管理者',
		groupleader => 'グループ管理者',
		normal => '一般ユーザ',
		limited => '制限ユーザ',
	);
	return $labels{$type} || $type;
}

sub localize_reason {
	my ($reason, $unused_days) = @_;
	if ($reason eq 'no_last_access') {
		return localize('neverAccessed');
	}
	if ($reason eq 'unused_days') {
		return localize('unusedDays', defined($unused_days) ? $unused_days : '');
	}
	if ($reason eq 'allowhosts_missing') {
		return localize('allowhostsMissing');
	}
	return $reason || '';
}

sub localize {
	my $key = shift || '';
	my @args = @_;
	my %ja = (
		title => '監査',
		subtitle => 'TeamFileの日常運用で確認すべきリスクを定期監査結果から確認します。',
		userAuditTitle => 'ユーザ監査',
		userAuditDesc => '未利用ユーザ、IP制御漏れ、管理者の設定漏れを定期監査結果から表示します。',
		fileAuditTitle => 'ファイル監査',
		fileAuditDesc => '容量逼迫、BOX外部公開、大容量ファイル、長期未更新ファイルを確認します。',
		oplogAuditTitle => '操作ログ監査',
		oplogAuditDesc => '実際の操作ログから、接続元IPの揺れ、大量操作、時間外操作などを確認します。',
		auditDeductionLabel => '減点',
		showLatestLabel => '最新結果を見る',
		preparingLabel => '準備中',
		noLatestResult => 'まだ監査結果がありません。次回の定期監査が完了するとここに表示されます。',
		latestMetaTitle => '最新のユーザ監査結果',
		generatedAtLabel => '生成日時',
		targetDateLabel => '監査対象日',
		statusLabel => '状態',
		elapsedLabel => '監査時間',
		summaryTotal => '登録ユーザ',
		summaryActive => '有効ユーザ',
		summaryAllowhostsRate => 'IP制御設定率',
		summaryUnused => '未利用候補',
		summaryAllowhostsMissing => 'IP制御未設定',
		summaryAdminAllowhostsMissing => '管理者系IP制御未設定',
		sectionIssues => '監査結果',
		sectionAdminAllowhosts => '重要: 管理者系のIP制御漏れ',
		sectionAllowhosts => 'IP制御未設定ユーザ',
		sectionUnused => '未利用ユーザ候補',
		sectionUnusedDesc => '有効ユーザのうち、最終アクセスが記録されていないユーザ、または長期間アクセスがないユーザを表示します。退職者、休眠アカウント、不要な共有IDが残っていないか確認してください。',
		colUserId => 'ユーザID',
		colName => '表示名',
		colMail => 'メールアドレス',
		colType => 'ユーザ種別',
		colState => '状態',
		colReason => '理由',
		colLastAccess => '最終アクセス',
		colLastClient => 'アクセス元クライアント',
		colAllowhosts => '許可IP',
		noRows => '該当なし',
		overallTitle => '運用リスク評価',
		overallDesc => '重要・警告・注意の件数から簡易的に算出した運用リスクの目安です。',
		overallScoreLabel => '評価スコア',
		rank_good => '良好',
		rank_watch => '要確認',
		rank_bad => '要対応',
		tabOverviewLabel => '総括',
		tabUserLabel => 'ユーザ監査',
		tabFileLabel => 'ファイル監査',
		tabOplogLabel => '操作ログ監査',
		overviewHeading => '監査サマリー',
		overviewUserNote => 'ユーザ監査では、未利用ユーザとIP制御漏れを確認します。',
		overviewFileNote => 'ファイル監査では、容量逼迫、BOX外部公開、大容量・長期未更新ファイルを確認します。',
		fileLatestMetaTitle => '最新のファイル監査結果',
		fileSummaryDiskUsage => '全体使用率',
		fileSummaryDiskUsed => '使用容量',
		fileSummaryBoxOpen => '公開中BOX',
		fileSummaryBoxRisk => 'BOX公開リスク',
		fileSummaryLarge => '大容量ファイル',
		fileSummaryStale => '長期未更新',
		sectionFileIssues => 'ファイル監査結果',
		sectionFileTopUsers => '個人別利用量上位',
		sectionFileInactiveUsers => '非アクティブユーザの残データ',
		sectionFileBoxExpired => '期限切れ公開中BOX',
		sectionFileBoxUnlimited => '期限なし公開BOX',
		sectionFileBoxLong => '長期公開BOX',
		sectionFileLarge => '大容量ファイル',
		sectionFileStale => '長期未更新ファイル',
		colUsedSize => '使用容量',
		colUsedFiles => 'ファイル数',
		colRate => '割合',
		colUri => 'URI',
		colBoxName => 'BOX名',
		colGroupName => 'グループ名',
		colToMail => '通知先',
		colMessage => 'メッセージ',
		colCreator => '作成者',
		colCreated => '作成日時',
		colExpiration => '公開期限',
		colViewcount => '参照数',
		colSize => 'サイズ',
		colLastModified => '最終更新',
		colStaleDays => '未更新日数',
		colLastModifier => '最終更新者',
		oplogLatestMetaTitle => '最新の操作ログ監査結果',
		oplogSummaryIndexedDays => '対象日数',
		oplogSummaryRows => '操作ログ件数',
		oplogSummaryGroups => '対象グループ',
		oplogSummaryIpDrift => 'IP揺れ',
		oplogSummaryBulkPut => '大量PUT',
		oplogSummaryBulkDelete => '大量DELETE',
		oplogSummaryAuthFailed => 'ログイン失敗',
		oplogSummaryAuthLocked => 'ロックアウト',
		oplogSummaryBulkGet => '大量GET',
		oplogSummaryDenied => '認可拒否集中',
		oplogSummaryWebdavMethods => 'WebDAV集中',
		oplogSummaryManagementChanges => '管理変更集中',
		oplogSummaryMissingIndexes => '未索引日',
		sectionOplogIssues => '操作ログ監査結果',
		sectionOplogAuthFailedUsers => 'ログイン失敗が目立つユーザ',
		sectionOplogAuthFailedIps => '複数ユーザで失敗している接続元IP',
		sectionOplogAuthLockouts => 'ロックアウト到達ユーザ',
		sectionOplogDailyIpDrift => '同一日の接続元IP揺れ',
		sectionOplogPeriodIpDrift => '期間内の接続元IP揺れ',
		oplogPeriodIpDriftDesc => '画面上部の対象日を終端とする過去%d日間（通常7日間）を集計します。5操作以上ある同一ユーザが3個以上の接続元IPを使用した場合に表示します。',
		sectionOplogBulkPut => '大量PUT',
		sectionOplogBulkDelete => '大量DELETE',
		sectionOplogOffhours => '時間外操作',
		oplogOffhoursDesc => 'サーバ時刻の0:00～7:59および20:00～23:59に行われたPUTまたはDELETEを対象とし、同一グループ・同一日で50件以上の場合に表示します。',
		sectionOplogFailedStatus => '失敗ステータス',
		sectionOplogBulkGet => '大量ダウンロード候補',
		sectionOplogDeniedUsers => '拒否が集中したユーザ',
		sectionOplogDeniedIps => '拒否が集中した接続元IP',
		sectionOplogWebdavMethods => 'WebDAVメソッド集中',
		sectionOplogManagementChanges => '管理リソース変更集中',
		sectionOplogMissingIndexes => '操作ログインデックス欠落',
		colDate => '日付',
		colGroup => 'グループ',
		colCount => '件数',
		colIpCount => 'IP数',
		colOperationCount => '操作数',
		colIps => '接続元IP',
		colCountedCount => 'カウント対象',
		colLockedCount => 'ロックアウト',
		colUserCount => 'ユーザ数',
		colFirstTime => '初回',
		colLastTime => '最終',
		colUsers => 'ユーザ',
		colCurrentFailedCount => '現在の失敗回数',
		colActiveStatus => '現在状態',
		colPeriodDays => '期間',
		colSubject => '対象',
		colPeerCount => '関連元数',
		colTargetCount => '対象種別数',
		colTargets => '対象種別',
		colMethods => 'メソッド内訳',
		unlimited => '無期限',
		active => '有効',
		inactive => '非アクティブ',
		neverAccessed => '最終アクセスなし',
		allowhostsMissing => 'IP制御未設定',
		level_ok => 'OK',
		level_caution => '注意',
		level_warn => '警告',
		level_critical => '重要',
		issue_admin_allowhosts_missing_title => '管理者系ユーザのIP制御漏れがあります',
		issue_admin_allowhosts_missing_body => '管理者またはグループ管理者でIP制御が未設定のユーザがあります。優先して確認してください。',
		issue_allowhosts_major_missing_title => 'IP制御が多数派の環境で未設定ユーザがあります',
		issue_allowhosts_major_missing_body => '多くのユーザにIP制御が設定されている環境で、未設定ユーザが残っています。',
		issue_unused_users_title => '未利用ユーザ候補があります',
		issue_unused_users_body => '長期間アクセスのない有効ユーザ、または最終アクセス不明の有効ユーザがあります。',
		issue_no_problem_title => '重大な指摘はありません',
		issue_no_problem_body => '現在の監査条件では重要な設定漏れは見つかりませんでした。',
		issue_disk_usage_critical_title => 'ストレージ全体の使用率が危険域です',
		issue_disk_usage_critical_body => '全体容量が90%以上使用されています。不要ファイル整理や容量追加を検討してください。',
		issue_disk_usage_warn_title => 'ストレージ全体の使用率が高くなっています',
		issue_disk_usage_warn_body => '全体容量が80%以上使用されています。容量推移を確認してください。',
		issue_disk_quota_unlimited_title => 'ストレージ容量上限が未設定です',
		issue_disk_quota_unlimited_body => '容量上限なしの運用です。異常ではありませんが、容量逼迫の検知が弱くなります。',
		issue_box_expired_open_title => '期限切れのBOXが公開中です',
		issue_box_expired_open_body => '公開期限を過ぎても公開状態のBOXがあります。優先して確認してください。',
		issue_box_unlimited_open_title => '期限なしで公開中のBOXがあります',
		issue_box_unlimited_open_body => '外部公開が無期限になっているBOXがあります。公開意図を確認してください。',
		issue_box_long_open_title => '長期公開予定のBOXがあります',
		issue_box_long_open_body => '公開期限が長く設定されたBOXがあります。公開期間が妥当か確認してください。',
		issue_huge_files_title => '極端に大きいファイルがあります',
		issue_huge_files_body => '500MB以上のファイルがあります。ダウンロードやバックアップ負荷に注意してください。',
		issue_large_files_title => '大容量ファイルがあります',
		issue_large_files_body => '100MB以上のファイルがあります。容量消費の要因として確認してください。',
		issue_very_stale_files_title => '1年以上更新されていないファイルがあります',
		issue_very_stale_files_body => '365日以上更新されていないファイルがあります。整理対象か確認してください。',
		issue_stale_files_title => '長期間更新されていないファイルがあります',
		issue_stale_files_body => '180日以上更新されていないファイルがあります。',
		issue_oplog_daily_ip_drift_title => '同一ユーザの接続元IPが1日の中で揺れています',
		issue_oplog_daily_ip_drift_body => '通常より多い接続元IPから同一ユーザの操作があります。VPN、プロキシ、共有ID利用の可能性を確認してください。',
		issue_oplog_period_ip_drift_warn_title => '同一ユーザの接続元IPが期間内で大きく揺れています',
		issue_oplog_period_ip_drift_warn_body => '期間内で接続元IPが多いユーザがあります。利用実態を確認してください。',
		issue_oplog_period_ip_drift_caution_title => '同一ユーザの接続元IPに揺れがあります',
		issue_oplog_period_ip_drift_caution_body => '期間内で複数の接続元IPから操作したユーザがあります。',
		issue_oplog_bulk_put_title => '大量のファイル登録があります',
		issue_oplog_bulk_put_body => '特定グループでPUT操作が多く発生しています。通常運用か確認してください。',
		issue_oplog_bulk_delete_critical_title => '大量削除が発生しています',
		issue_oplog_bulk_delete_critical_body => '特定グループで非常に多いDELETE操作があります。誤削除や不正操作でないか確認してください。',
		issue_oplog_bulk_delete_title => '削除操作が多く発生しています',
		issue_oplog_bulk_delete_body => '特定グループでDELETE操作が多く発生しています。',
		issue_oplog_offhours_activity_title => '時間外の更新・削除操作があります',
		issue_oplog_offhours_activity_body => '夜間帯にPUTまたはDELETEがまとまって発生しています。',
		issue_oplog_failed_status_title => '失敗ステータスが多く発生しています',
		issue_oplog_failed_status_body => '操作ログ上で失敗ステータスが多いグループがあります。',
		issue_oplog_bulk_get_critical_title => '極端に多いダウンロードがあります',
		issue_oplog_bulk_get_critical_body => '同一ユーザのGET件数または応答量が重要閾値を超えています。情報持ち出しや同期処理の妥当性を確認してください。',
		issue_oplog_bulk_get_title => 'ダウンロード量が増加しています',
		issue_oplog_bulk_get_body => '同一ユーザのGET件数または応答量が警告閾値を超えています。通常利用か確認してください。',
		issue_oplog_denied_probe_title => '認可拒否が繰り返されています',
		issue_oplog_denied_probe_body => '同一ユーザまたは接続元IPから複数種別への401、403、404が集中しています。探索や設定誤りを確認してください。',
		issue_oplog_webdav_method_title => 'WebDAVメソッドが集中しています',
		issue_oplog_webdav_method_body => '通常より多い探索、移動、コピー、プロパティ操作、ロック操作があります。クライアント動作か確認してください。',
		issue_oplog_management_change_title => '管理変更が集中しています',
		issue_oplog_management_change_body => 'ユーザ、グループ等の管理リソースに対する変更が集中しています。計画された管理作業か確認してください。',
		issue_oplog_missing_index_title => '操作ログインデックスに欠落があります',
		issue_oplog_missing_index_body => 'access logが存在する一方で操作ログインデックスが完成していない日があります。監査ジョブを確認してください。',
		issue_oplog_auth_lockout_title => 'ログインロックアウトに到達したユーザがあります',
		issue_oplog_auth_lockout_body => '認証失敗によりロックアウトしたユーザがあります。本人操作か、不正な試行か確認してください。',
		issue_oplog_auth_failed_critical_title => 'ログイン失敗が高頻度に発生しています',
		issue_oplog_auth_failed_critical_body => '同一ユーザで重要閾値を超える認証失敗があります。パスワード誤入力、古い資格情報、不正試行の可能性を確認してください。',
		issue_oplog_auth_failed_warn_title => 'ログイン失敗が繰り返されています',
		issue_oplog_auth_failed_warn_body => '同一ユーザで複数回の認証失敗があります。',
		issue_oplog_auth_failed_ip_title => '同一IPから複数ユーザのログイン失敗があります',
		issue_oplog_auth_failed_ip_body => '同じ接続元IPから複数ユーザへの認証失敗があります。攻撃や共有端末の問題を確認してください。',
	);
	my %en = (
		title => 'Audit',
		subtitle => 'Review TeamFile operational risks from scheduled audit results.',
		userAuditTitle => 'User Audit',
		userAuditDesc => 'Show scheduled audit results for unused users, missing IP restrictions, and administrator configuration gaps.',
		fileAuditTitle => 'File Audit',
		fileAuditDesc => 'Check storage pressure, public BOX entries, large files, and stale files.',
		oplogAuditTitle => 'Operation Log Audit',
		oplogAuditDesc => 'Check actual operation logs for source IP drift, bulk operations, and off-hours activity.',
		auditDeductionLabel => 'Deduction',
		showLatestLabel => 'View Latest Result',
		preparingLabel => 'Preparing',
		noLatestResult => 'No audit result is available yet. Results will appear here after the next scheduled audit finishes.',
		latestMetaTitle => 'Latest User Audit Result',
		generatedAtLabel => 'Generated at',
		targetDateLabel => 'Target date',
		statusLabel => 'Status',
		elapsedLabel => 'Audit time',
		summaryTotal => 'Total users',
		summaryActive => 'Active users',
		summaryAllowhostsRate => 'IP restriction rate',
		summaryUnused => 'Unused candidates',
		summaryAllowhostsMissing => 'Missing IP restriction',
		summaryAdminAllowhostsMissing => 'Admin missing IP restriction',
		sectionIssues => 'Audit Results',
		sectionAdminAllowhosts => 'Critical: Admin IP restriction gaps',
		sectionAllowhosts => 'Users without IP restrictions',
		sectionUnused => 'Unused user candidates',
		sectionUnusedDesc => 'Shows active users with no recorded last access or no access for a long period. Check for retired users, dormant accounts, or unnecessary shared IDs.',
		colUserId => 'User ID',
		colName => 'Name',
		colMail => 'Mail address',
		colType => 'User type',
		colState => 'State',
		colReason => 'Reason',
		colLastAccess => 'Last access',
		colLastClient => 'Last client',
		colAllowhosts => 'Allowed IPs',
		noRows => 'No matches',
		overallTitle => 'Operational Risk Evaluation',
		overallDesc => 'A simple risk score calculated from critical, warning, and caution findings.',
		overallScoreLabel => 'Risk score',
		rank_good => 'Good',
		rank_watch => 'Review',
		rank_bad => 'Action required',
		tabOverviewLabel => 'Overview',
		tabUserLabel => 'User Audit',
		tabFileLabel => 'File Audit',
		tabOplogLabel => 'Operation Log Audit',
		overviewHeading => 'Audit Summary',
		overviewUserNote => 'User audit checks unused users and missing IP restrictions.',
		overviewFileNote => 'File audit checks storage pressure, public BOX entries, large files, and stale files.',
		fileLatestMetaTitle => 'Latest File Audit Result',
		fileSummaryDiskUsage => 'Storage usage',
		fileSummaryDiskUsed => 'Used storage',
		fileSummaryBoxOpen => 'Open BOX',
		fileSummaryBoxRisk => 'BOX risks',
		fileSummaryLarge => 'Large files',
		fileSummaryStale => 'Stale files',
		sectionFileIssues => 'File Audit Results',
		sectionFileTopUsers => 'Top User Storage Usage',
		sectionFileInactiveUsers => 'Inactive Users with Stored Data',
		sectionFileBoxExpired => 'Expired Open BOX Entries',
		sectionFileBoxUnlimited => 'Open BOX Entries without Expiration',
		sectionFileBoxLong => 'Long-Term Open BOX Entries',
		sectionFileLarge => 'Large Files',
		sectionFileStale => 'Stale Files',
		colUsedSize => 'Used size',
		colUsedFiles => 'Files',
		colRate => 'Rate',
		colUri => 'URI',
		colBoxName => 'BOX name',
		colGroupName => 'Group name',
		colToMail => 'To mail',
		colMessage => 'Message',
		colCreator => 'Creator',
		colCreated => 'Created',
		colExpiration => 'Expiration',
		colViewcount => 'Views',
		colSize => 'Size',
		colLastModified => 'Last modified',
		colStaleDays => 'Stale days',
		colLastModifier => 'Last modifier',
		oplogLatestMetaTitle => 'Latest Operation Log Audit Result',
		oplogSummaryIndexedDays => 'Indexed days',
		oplogSummaryRows => 'Operation rows',
		oplogSummaryGroups => 'Groups',
		oplogSummaryIpDrift => 'IP drift',
		oplogSummaryBulkPut => 'Bulk PUT',
		oplogSummaryBulkDelete => 'Bulk DELETE',
		oplogSummaryAuthFailed => 'Login failures',
		oplogSummaryAuthLocked => 'Lockouts',
		oplogSummaryBulkGet => 'Bulk GET',
		oplogSummaryDenied => 'Denied bursts',
		oplogSummaryWebdavMethods => 'WebDAV bursts',
		oplogSummaryManagementChanges => 'Management bursts',
		oplogSummaryMissingIndexes => 'Missing indexes',
		sectionOplogIssues => 'Operation Log Audit Results',
		sectionOplogAuthFailedUsers => 'Users with repeated login failures',
		sectionOplogAuthFailedIps => 'Source IPs failing for multiple users',
		sectionOplogAuthLockouts => 'Users reaching lockout',
		sectionOplogDailyIpDrift => 'Daily Source IP Drift',
		sectionOplogPeriodIpDrift => 'Period Source IP Drift',
		oplogPeriodIpDriftDesc => 'Aggregates the %d days ending on the target date shown above (normally 7 days). A user is listed after at least 5 operations from 3 or more source IP addresses.',
		sectionOplogBulkPut => 'Bulk PUT',
		sectionOplogBulkDelete => 'Bulk DELETE',
		sectionOplogOffhours => 'Off-Hours Operations',
		oplogOffhoursDesc => 'Counts PUT and DELETE operations from 00:00 through 07:59 and from 20:00 through 23:59 in server local time. A group-day is listed at 50 or more operations.',
		sectionOplogFailedStatus => 'Failed Statuses',
		sectionOplogBulkGet => 'Bulk Download Candidates',
		sectionOplogDeniedUsers => 'Users with Repeated Denials',
		sectionOplogDeniedIps => 'Source IPs with Repeated Denials',
		sectionOplogWebdavMethods => 'Concentrated WebDAV Methods',
		sectionOplogManagementChanges => 'Concentrated Management Changes',
		sectionOplogMissingIndexes => 'Missing Operation Log Indexes',
		colDate => 'Date',
		colGroup => 'Group',
		colCount => 'Count',
		colIpCount => 'IPs',
		colOperationCount => 'Operations',
		colIps => 'Source IPs',
		colCountedCount => 'Counted',
		colLockedCount => 'Lockouts',
		colUserCount => 'Users',
		colFirstTime => 'First',
		colLastTime => 'Last',
		colUsers => 'Users',
		colCurrentFailedCount => 'Current failures',
		colActiveStatus => 'Current status',
		colPeriodDays => 'Period',
		colSubject => 'Subject',
		colPeerCount => 'Related peers',
		colTargetCount => 'Target classes',
		colTargets => 'Target classes',
		colMethods => 'Methods',
		unlimited => 'Unlimited',
		active => 'Active',
		inactive => 'Inactive',
		neverAccessed => 'No last access',
		allowhostsMissing => 'Missing IP restriction',
		level_ok => 'OK',
		level_caution => 'Caution',
		level_warn => 'Warning',
		level_critical => 'Critical',
		issue_admin_allowhosts_missing_title => 'Administrators without IP restrictions exist',
		issue_admin_allowhosts_missing_body => 'Administrator or group leader users without IP restrictions exist. Review these first.',
		issue_allowhosts_major_missing_title => 'Some users lack IP restrictions in an IP-restricted environment',
		issue_allowhosts_major_missing_body => 'Most users have IP restrictions, but some active users remain unrestricted.',
		issue_unused_users_title => 'Unused user candidates exist',
		issue_unused_users_body => 'Active users with no long-term access or unknown last access exist.',
		issue_no_problem_title => 'No major findings',
		issue_no_problem_body => 'No critical configuration gaps were found with the current audit rules.',
		issue_disk_usage_critical_title => 'Storage usage is critical',
		issue_disk_usage_critical_body => 'Storage usage is 90% or higher. Review cleanup or capacity expansion.',
		issue_disk_usage_warn_title => 'Storage usage is high',
		issue_disk_usage_warn_body => 'Storage usage is 80% or higher. Review the capacity trend.',
		issue_disk_quota_unlimited_title => 'Storage quota is unlimited',
		issue_disk_quota_unlimited_body => 'This is not necessarily an error, but storage pressure is harder to detect.',
		issue_box_expired_open_title => 'Expired BOX entries are still open',
		issue_box_expired_open_body => 'Some BOX entries are still public after expiration. Review these first.',
		issue_box_unlimited_open_title => 'BOX entries without expiration are open',
		issue_box_unlimited_open_body => 'Some external BOX publications have no expiration date.',
		issue_box_long_open_title => 'Long-term public BOX entries exist',
		issue_box_long_open_body => 'Some BOX entries have long public expiration periods. Review whether they are intentional.',
		issue_huge_files_title => 'Very large files exist',
		issue_huge_files_body => 'Files larger than 500MB exist. Watch download and backup load.',
		issue_large_files_title => 'Large files exist',
		issue_large_files_body => 'Files larger than 100MB exist. Review them as storage consumers.',
		issue_very_stale_files_title => 'Files not updated for over a year exist',
		issue_very_stale_files_body => 'Files not updated for 365+ days exist. Review whether they should be archived or removed.',
		issue_stale_files_title => 'Long-stale files exist',
		issue_stale_files_body => 'Files not updated for 180+ days exist.',
		issue_oplog_daily_ip_drift_title => 'A user used many source IPs in one day',
		issue_oplog_daily_ip_drift_body => 'A user operated from more source IPs than usual. Review VPN, proxy, or shared account usage.',
		issue_oplog_period_ip_drift_warn_title => 'A user has high source IP drift in the period',
		issue_oplog_period_ip_drift_warn_body => 'Some users operated from many source IPs during the audit period.',
		issue_oplog_period_ip_drift_caution_title => 'A user has source IP drift',
		issue_oplog_period_ip_drift_caution_body => 'Some users operated from multiple source IPs during the audit period.',
		issue_oplog_bulk_put_title => 'Bulk file registration occurred',
		issue_oplog_bulk_put_body => 'Many PUT operations occurred in specific groups. Review whether this is normal.',
		issue_oplog_bulk_delete_critical_title => 'Bulk deletion occurred',
		issue_oplog_bulk_delete_critical_body => 'Very many DELETE operations occurred in specific groups. Review accidental or unauthorized deletion.',
		issue_oplog_bulk_delete_title => 'Many delete operations occurred',
		issue_oplog_bulk_delete_body => 'Many DELETE operations occurred in specific groups.',
		issue_oplog_offhours_activity_title => 'Off-hours update/delete activity occurred',
		issue_oplog_offhours_activity_body => 'PUT or DELETE operations occurred in bulk at night.',
		issue_oplog_failed_status_title => 'Many failed statuses occurred',
		issue_oplog_failed_status_body => 'Some groups have many failed operation statuses.',
		issue_oplog_bulk_get_critical_title => 'Extremely large downloads occurred',
		issue_oplog_bulk_get_critical_body => 'A user exceeded the critical GET count or response volume threshold. Review possible extraction or expected synchronization.',
		issue_oplog_bulk_get_title => 'Download volume increased',
		issue_oplog_bulk_get_body => 'A user exceeded the warning GET count or response volume threshold. Confirm normal usage.',
		issue_oplog_denied_probe_title => 'Authorization denials were repeated',
		issue_oplog_denied_probe_body => 'A user or source IP generated concentrated 401, 403, or 404 responses across target classes. Review probing or configuration errors.',
		issue_oplog_webdav_method_title => 'WebDAV methods were concentrated',
		issue_oplog_webdav_method_body => 'Many discovery, move, copy, property, or lock operations occurred. Confirm expected client behavior.',
		issue_oplog_management_change_title => 'Management changes were concentrated',
		issue_oplog_management_change_body => 'Many user or group management resource changes occurred. Confirm planned administration.',
		issue_oplog_missing_index_title => 'Operation log indexes are missing',
		issue_oplog_missing_index_body => 'Access logs exist for days whose operation log indexes are incomplete. Review the indexing job.',
		issue_oplog_auth_lockout_title => 'Users reached login lockout',
		issue_oplog_auth_lockout_body => 'Some users reached lockout due to failed authentication. Review whether this was expected.',
		issue_oplog_auth_failed_critical_title => 'Many login failures occurred',
		issue_oplog_auth_failed_critical_body => 'A user has many authentication failures. Review wrong passwords, stale credentials, or unauthorized attempts.',
		issue_oplog_auth_failed_warn_title => 'Repeated login failures occurred',
		issue_oplog_auth_failed_warn_body => 'A user has repeated authentication failures.',
		issue_oplog_auth_failed_ip_title => 'A source IP failed login for multiple users',
		issue_oplog_auth_failed_ip_body => 'The same source IP failed authentication for multiple users. Review possible attacks or shared terminal issues.',
	);

	if ($key eq 'unusedDays') {
		return ($LANGPARAM eq 'ja') ? "$args[0]日以上未利用" : "Unused for $args[0]+ days";
	}

	return ($LANGPARAM eq 'ja') ? ($ja{$key} || $key) : ($en{$key} || $key);
}
