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
30 changes: 22 additions & 8 deletions app/components/Analysis/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const { data, status, error } = useFetch(

const { data: verifiedAutomations } = useVerifiedAutomations();

const verifiedAutomation = computed(() => {
const verifiedAutomation = computed<VerifiedAutomation | undefined>(() => {
return verifiedAutomations.value?.find((account) => {
return (
account.username.toLowerCase() === username.value?.toLowerCase() ||
Expand All @@ -34,6 +34,14 @@ const verifiedAutomation = computed(() => {
});
});

const { data: integrations } = useIntegrations();
const activityReport = computed<IntegrationItem | undefined>(() => {
return integrations.value?.find((item) => {
return item.username.toLowerCase() === username.value?.toLowerCase();
});
});

const hasActivityReport = computed<boolean>(() => !!activityReport.value);
const hasCommunityFlag = computed<boolean>(() => !!verifiedAutomation.value);

const flagCreatedAt = computed<string | undefined>(() => {
Expand Down Expand Up @@ -70,23 +78,23 @@ const scoreStyle = computed<ScoreStyle>(() => {
};
}

if (classification.value === "organic") {
if (classification.value === "automation") {
return {
text: "text-green-500",
border: "border-green-500",
text: "text-orange-500",
border: "border-orange-500",
};
}

if (classification.value === "mixed") {
if (classification.value === "mixed" || hasActivityReport) {
return {
text: "text-amber-500",
border: "border-amber-500",
};
}

return {
text: "text-orange-500",
border: "border-orange-500",
text: "text-green-500",
border: "border-green-500",
};
});

Expand Down Expand Up @@ -213,7 +221,7 @@ useSeoAnalysis(identifyAnalysis, {
</div>

<div
v-if="data.analysis.flags.length > 0"
v-if="data.analysis.flags.length > 0 || hasActivityReport"
class="bg-gh-card p-6 rounded-2 border-1 border-solid border-gh-border"
>
<h3 class="mb-4 text-gh-text text-xl font-mono">Activity Signals</h3>
Expand All @@ -229,6 +237,12 @@ useSeoAnalysis(identifyAnalysis, {
</p>
</li>
</ul>

<ExternalAnlysisCard
v-if="activityReport"
:items="[activityReport]"
class="mt-4"
/>
</div>

<ChartAccountEventsTimeline
Expand Down
70 changes: 70 additions & 0 deletions app/components/ExternalAnlysis/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<script setup lang="ts">
const props = defineProps<{
items: IntegrationItem[];
}>();
Comment thread
MatteoGabriele marked this conversation as resolved.

const isDisclosureOpen = ref<boolean>(false);
const counter = computed<number>(() => {
return props.items.length;
});
</script>

<template>
<section>
<button
@click="isDisclosureOpen = !isDisclosureOpen"
:aria-expanded="isDisclosureOpen"
aria-controls="disclosure-external-analysis"
class="w-full bg-amber-500/10 text-amber-500 rounded-lg border-amber-500/40 border px-3 py-2 text-left transition-colors"
:class="{
'border-b-none rounded-b-none': isDisclosureOpen,
'hover:border-amber-500': !isDisclosureOpen,
}"
>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div class="flex items-center justify-between">
<h3 class="flex items-center gap-2 text-sm">
<span class="i-carbon:warning"></span>
<span>Suspicious Activity Reported</span>
</h3>
<div class="flex items-center gap-3">
<span
class="bg-amber-500/20 text-amber-500 text-xs font-semibold px-2 py-1 rounded"
>
{{ counter }}
</span>
<span
:class="[
'i-carbon:chevron-down text-base transition-transform',
isDisclosureOpen && 'rotate-180',
]"
/>
</div>
</div>
</button>

<ul
v-if="isDisclosureOpen"
id="disclosure-external-analysis"
class="bg-amber-500/5 border border-t-amber-500/30 rounded-b-md border-amber-500/40 p-4 space-y-4"
>
<li
v-for="item in items"
:key="`${item.username}-${item.link}`"
class="p-3 space-y-2"
>
<h4 class="text-gh-text/90 text-sm">{{ item.label }}</h4>
<p class="text-gh-text/70 text-sm">
{{ item.reason }}
</p>
<NuxtLink
external
:to="item.link"
class="inline-block text-gh-text/80 underline text-xs font-semibold hover:text-gh-text"
target="_blank"
>
View Report →
</NuxtLink>
</li>
</ul>
</section>
</template>
7 changes: 7 additions & 0 deletions app/composables/useIntegrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { IntegrationItem } from "~~/shared/types/integrations";

export function useIntegrations() {
return useLazyAsyncData("integrations", async () => {
return $fetch<IntegrationItem[]>("/api/integration/unsafe-labs");
});
}
43 changes: 43 additions & 0 deletions server/api/integration/unsafe-labs.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Octokit } from "octokit";
import dayjs from "dayjs";
import type { IntegrationItem } from "~~/shared/types/integrations";

export type IntegrationUsafeLab = {
username: string;
total_prs: number;
first_pr: string;
last_pr: string;
};

export default defineEventHandler(async () => {
const config = useRuntimeConfig();
const octokit = new Octokit({ auth: config.githubToken });

try {
const { data } = await octokit.rest.repos.getContent({
owner: "UnsafeLabs",
repo: "Bounty-Hunters",
path: "clankers.json",
});

if ("content" in data) {
const content = Buffer.from(data.content, "base64").toString("utf-8");
const integrationData = JSON.parse(content) as IntegrationUsafeLab[];
return integrationData.map((d) => ({
label: "UnsafeLabs Bounty Hunters",
username: d.username,
createdAt: d.first_pr,
reason: `This account appears in the UnsafeLabs bounty hunters database. Submitted a total of ${d.total_prs} PR${d.total_prs === 1 ? "" : "s"} to the project. Activity detected from ${dayjs(d.first_pr).format("MMM D, YYYY")} through ${dayjs(d.last_pr).format("MMM D, YYYY")}.`,
link: `https://github.com/UnsafeLabs/Bounty-Hunters/pulls?q=is%3Apr+author%3A${d.username}`,
})) satisfies IntegrationItem[];
}

return [];
} catch (error) {
throw createError({
statusCode: 500,
message: "Failed to fetch integration list",
cause: error,
});
}
});
7 changes: 7 additions & 0 deletions shared/types/integrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type IntegrationItem = {
label: string;
username: string;
createdAt?: string;
reason: string;
link: string;
};